From d4585be7cf4b6fbce85a6fa7a5b448b604036f86 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 14 Jul 2024 15:21:08 +0100 Subject: [PATCH 001/250] Linux: Update vmayarascan to scan complete VMA blocks --- .../framework/plugins/linux/vmayarascan.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index eda0d7dcaf..3edbe7784d 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -33,9 +33,6 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0) ), - requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) - ), requirements.ModuleRequirement( name="kernel", description="Linux kernel", @@ -69,19 +66,29 @@ def _generator(self): # get the proc_layer object from the context proc_layer = self.context.layers[proc_layer_name] - # scan the process layer with the yarascanner - for offset, rule_name, name, value in proc_layer.scan( - context=self.context, - scanner=yarascan.YaraScanner(rules=rules), - sections=self.get_vma_maps(task), - ): - yield 0, ( - format_hints.Hex(offset), - task.tgid, - rule_name, - name, - value, - ) + for start, end in self.get_vma_maps(task): + for match in rules.match( + data=proc_layer.read(start, end - start, True) + ): + if yarascan.YaraScan.yara_returns_instances(): + for match_string in match.strings: + for instance in match_string.instances: + yield 0, ( + format_hints.Hex(instance.offset + start), + task.UniqueProcessId, + match.rule, + match_string.identifier, + instance.matched_data, + ) + else: + for offset, name, value in match.strings: + yield 0, ( + format_hints.Hex(offset + start), + task.tgid, + match.rule, + name, + value, + ) @staticmethod def get_vma_maps( From bca4cdda744120a5ca868bdfd39119ebf0819201 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Fri, 19 Jul 2024 11:09:52 +0200 Subject: [PATCH 002/250] replace yara-python with yara-x --- doc/requirements.txt | 2 +- test/requirements-testing.txt | 2 +- volatility3/framework/plugins/yarascan.py | 74 ++++++----------------- 3 files changed, 22 insertions(+), 56 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index b715e59f58..c04f712198 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,6 +3,6 @@ sphinx>=4.0.0,<7 sphinx_autodoc_typehints>=1.4.0 sphinx-rtd-theme>=0.4.3 -yara-python +yara-x pycryptodome pefile diff --git a/test/requirements-testing.txt b/test/requirements-testing.txt index 7afe19b945..0955d19396 100644 --- a/test/requirements-testing.txt +++ b/test/requirements-testing.txt @@ -5,6 +5,6 @@ pefile>=2017.8.1 #foo # If certain packages are not necessary, place a comment (#) at the start of the line. # This is required for the yara plugins -yara-python>=3.8.0 +yara-x>=0.5.0 pytest>=7.0.0 diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 496ec18446..32a400f9e4 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -14,14 +14,9 @@ vollog = logging.getLogger(__name__) try: - import yara + import yara_x - if tuple([int(x) for x in yara.__version__.split(".")]) < (3, 8): - raise ImportError except ImportError: - vollog.info( - "Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available" - ) raise @@ -34,27 +29,20 @@ def __init__(self, rules) -> None: if rules is None: raise ValueError("No rules provided to YaraScanner") self._rules = rules - self.st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < ( - 4, - 3, - ) def __call__( self, data: bytes, data_offset: int ) -> Iterable[Tuple[int, str, str, bytes]]: - for match in self._rules.match(data=data): - if YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield ( - instance.offset + data_offset, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield (offset + data_offset, match.rule, name, value) + results = self._rules.scan(data) + for match in results.matching_rules: + for match_string in match.patterns: + for instance in match_string.matches: + yield ( + instance.offset + data_offset, + f"{match.namespace}.{match.identifier}", + match_string.identifier, + data[instance.offset : instance.offset + instance.length], + ) class YaraScan(plugins.PluginInterface): @@ -63,9 +51,6 @@ class YaraScan(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 3, 0) - # TODO: When the major version is bumped, take the opportunity to rename the yara_rules config to yara_string - # or something that makes more sense - @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: """Returns the requirements needed to run yarascan directly, combining the TranslationLayerRequirement @@ -99,16 +84,13 @@ def get_yarascan_option_requirements( optional=True, ), requirements.StringRequirement( - name="yara_rules", description="Yara rules (as a string)", optional=True - ), - requirements.URIRequirement( - name="yara_file", description="Yara rules (as a file)", optional=True + name="yara_string", + description="Yara rules (as a string)", + optional=True, ), - # This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code - # As such, there's a separate option to run compiled files, as happened with yara-3.9 and later requirements.URIRequirement( name="yara_compiled_file", - description="Yara compiled rules (as a file)", + description="Yara-x compiled rules (as a file)", optional=True, ), requirements.IntRequirement( @@ -119,36 +101,20 @@ def get_yarascan_option_requirements( ), ] - @classmethod - def yara_returns_instances(cls) -> bool: - st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < ( - 4, - 3, - ) - return st_object - @classmethod def process_yara_options(cls, config: Dict[str, Any]): rules = None - if config.get("yara_rules", None) is not None: - rule = config["yara_rules"] + if config.get("yara_string") is not None: + rule = config["yara_string"] if rule[0] not in ["{", "/"]: rule = f'"{rule}"' if config.get("case", False): rule += " nocase" if config.get("wide", False): rule += " wide ascii" - rules = yara.compile( - sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} - ) - elif config.get("yara_source", None) is not None: - rules = yara.compile(source=config["yara_source"]) - elif config.get("yara_file", None) is not None: - rules = yara.compile( - file=resources.ResourceAccessor().open(config["yara_file"], "rb") - ) - elif config.get("yara_compiled_file", None) is not None: - rules = yara.load( + rules = yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") + elif config.get("yara_compiled_file") is not None: + rules = yara_x.Rules.deserialize_from( file=resources.ResourceAccessor().open( config["yara_compiled_file"], "rb" ) From ce9832a2b3d524637a7bfaa49c7726ff234c121e Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Mon, 22 Jul 2024 13:58:33 +0200 Subject: [PATCH 003/250] both yara-python and yara-x support --- doc/requirements.txt | 1 + test/requirements-testing.txt | 1 + volatility3/framework/plugins/yarascan.py | 101 ++++++++++++++++++++-- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index c04f712198..d3ba512245 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,6 +3,7 @@ sphinx>=4.0.0,<7 sphinx_autodoc_typehints>=1.4.0 sphinx-rtd-theme>=0.4.3 +yara-python yara-x pycryptodome pefile diff --git a/test/requirements-testing.txt b/test/requirements-testing.txt index 0955d19396..51c8f602c2 100644 --- a/test/requirements-testing.txt +++ b/test/requirements-testing.txt @@ -5,6 +5,7 @@ pefile>=2017.8.1 #foo # If certain packages are not necessary, place a comment (#) at the start of the line. # This is required for the yara plugins +yara-python>=3.8.0 yara-x>=0.5.0 pytest>=7.0.0 diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 32a400f9e4..03a45b8dfb 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -13,14 +13,73 @@ vollog = logging.getLogger(__name__) +USE_YARA_X = False + try: import yara_x + USE_YARA_X = True + except ImportError: - raise + try: + import yara + + if tuple(int(x) for x in yara.__version__.split(".")) < (3, 8): + raise ImportError + vollog.info("Using yara-python module") + + except ImportError: + vollog.info( + "Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available" + ) + raise -class YaraScanner(interfaces.layers.ScannerInterface): + +class YaraPythonScanner(interfaces.layers.ScannerInterface): + _version = (2, 0, 0) + + # yara.Rules isn't exposed, so we can't type this properly + def __init__(self, rules) -> None: + super().__init__() + if rules is None: + raise ValueError("No rules provided to YaraScanner") + self._rules = rules + self.st_object = not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3) + + def __call__( + self, data: bytes, data_offset: int + ) -> Iterable[Tuple[int, str, str, bytes]]: + for match in self._rules.match(data=data): + if self.st_object: + for match_string in match.strings: + for instance in match_string.instances: + yield ( + instance.offset + data_offset, + match.rule, + match_string.identifier, + instance.matched_data, + ) + else: + for offset, name, value in match.strings: + yield (offset + data_offset, match.rule, name, value) + + @staticmethod + def get_rule(rule): + return yara.compile( + sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} + ) + + @staticmethod + def from_compiled_file(filepath): + return yara.load(file=resources.ResourceAccessor().open(filepath, "rb")) + + @staticmethod + def from_file(filepath): + return yara.compile(file=resources.ResourceAccessor().open(filepath, "rb")) + + +class YaraXScanner(interfaces.layers.ScannerInterface): _version = (2, 0, 0) # yara.Rules isn't exposed, so we can't type this properly @@ -44,6 +103,25 @@ def __call__( data[instance.offset : instance.offset + instance.length], ) + @staticmethod + def get_rule(rule): + return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") + + @staticmethod + def from_compiled_file(filepath): + return yara_x.Rules.deserialize_from( + file=resources.ResourceAccessor().open(filepath, "rb") + ) + + @staticmethod + def from_file(filepath): + return yara_x.compile( + resources.ResourceAccessor().open(filepath, "rb").read().decode() + ) + + +YaraScanner = YaraXScanner if USE_YARA_X else YaraPythonScanner + class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" @@ -88,9 +166,14 @@ def get_yarascan_option_requirements( description="Yara rules (as a string)", optional=True, ), + requirements.URIRequirement( + name="yara_file", + description="Yara rules (as a file)", + optional=True, + ), requirements.URIRequirement( name="yara_compiled_file", - description="Yara-x compiled rules (as a file)", + description="Yara compiled rules (as a file)", optional=True, ), requirements.IntRequirement( @@ -112,13 +195,15 @@ def process_yara_options(cls, config: Dict[str, Any]): rule += " nocase" if config.get("wide", False): rule += " wide ascii" - rules = yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") + rules = YaraScanner.get_rule(rule) + elif config.get("yara_file") is not None: + vollog.debug(f"Plain file: {config["yara_file"]} - yara-x: {USE_YARA_X}") + rules = YaraScanner.from_file(config["yara_file"]) elif config.get("yara_compiled_file") is not None: - rules = yara_x.Rules.deserialize_from( - file=resources.ResourceAccessor().open( - config["yara_compiled_file"], "rb" - ) + vollog.debug( + f"Compiled file: {config["yara_compiled_file"]} - yara-x: {USE_YARA_X}" ) + rules = YaraScanner.from_compiled_file(config["yara_compiled_file"]) else: vollog.error("No yara rules, nor yara rules file were specified") return rules From af2d6206763a661ced9687f1c7b31b7888d3d0ce Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Tue, 23 Jul 2024 22:02:34 +0200 Subject: [PATCH 004/250] Improving lsof --- volatility3/framework/plugins/linux/lsof.py | 55 +++++++++++++++++-- .../framework/symbols/linux/__init__.py | 23 +++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index d970ad8a95..f9aeafe146 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -3,7 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" -import logging +import logging, datetime from typing import List, Callable from volatility3.framework import renderers, interfaces, constants @@ -76,14 +76,59 @@ def _generator(self, pids, symbol_table): ) for pid, task_comm, _task, fd_fields in fds_generator: - fd_num, _filp, full_path = fd_fields - - fields = (pid, task_comm, fd_num, full_path) + ( + fd_num, + _filp, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) = fd_fields + + fields = ( + pid, + task_comm, + fd_num, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) yield (0, fields) def run(self): pids = self.config.get("pid", None) symbol_table = self.config["kernel"] - tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)] + tree_grid_args = [ + ("PID", int), + ("Process", str), + ("FD", int), + ("Path", str), + ("Inode", int), + ("Mode", str), + ("LastChange", datetime.datetime), + ("LastModify", datetime.datetime), + ("LastAccessed", datetime.datetime), + ("Size", int), + ] return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) + + def generate_timeline(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + for row in self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ): + _depth, row_data = row + description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[4]}"' + yield description, timeliner.TimeLinerType.CHANGED, row_data[5] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[6] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[7] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index c4e2587f49..b9321f3693 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,6 +1,7 @@ # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import stat, datetime from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework @@ -265,8 +266,26 @@ def files_descriptors_for_process( for fd_num, filp in enumerate(fds): if filp != 0: full_path = LinuxUtilities.path_for_file(context, task, filp) - - yield fd_num, filp, full_path + dentry = filp.get_dentry() + if dentry != 0: + inode_object = dentry.d_inode + inode_num = inode_object.i_ino + file_size = inode_object.i_size # file size in bytes + imode = stat.filemode( + inode_object.i_mode + ) # file type & Permissions + + # Timestamps + ctime = datetime.datetime.fromtimestamp( + inode_object.i_ctime.tv_sec + ) # last change time + mtime = datetime.datetime.fromtimestamp( + inode_object.i_mtime.tv_sec + ) # last modify time + atime = datetime.datetime.fromtimestamp( + inode_object.i_atime.tv_sec + ) # last access time + yield fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size @classmethod def mask_mods_list( From fceb79ba8b86e95919451ee29ca9fdfccd3ebc3e Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 25 Jul 2024 08:34:04 +0200 Subject: [PATCH 005/250] use with context, bump release --- volatility3/framework/plugins/yarascan.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 03a45b8dfb..7957aa0c53 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -27,11 +27,11 @@ if tuple(int(x) for x in yara.__version__.split(".")) < (3, 8): raise ImportError - vollog.info("Using yara-python module") + vollog.debug("Using yara-python module") except ImportError: vollog.info( - "Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available" + "Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available" ) raise @@ -51,7 +51,7 @@ def __call__( self, data: bytes, data_offset: int ) -> Iterable[Tuple[int, str, str, bytes]]: for match in self._rules.match(data=data): - if self.st_object: + if YaraScan.yara_returns_instances(): for match_string in match.strings: for instance in match_string.instances: yield ( @@ -72,11 +72,13 @@ def get_rule(rule): @staticmethod def from_compiled_file(filepath): - return yara.load(file=resources.ResourceAccessor().open(filepath, "rb")) + with resources.ResourceAccessor().open(filepath, "rb") as fp: + return yara.load(file=fp) @staticmethod def from_file(filepath): - return yara.compile(file=resources.ResourceAccessor().open(filepath, "rb")) + with resources.ResourceAccessor().open(filepath, "rb") as fp: + return yara.compile(file=fp) class YaraXScanner(interfaces.layers.ScannerInterface): @@ -109,15 +111,13 @@ def get_rule(rule): @staticmethod def from_compiled_file(filepath): - return yara_x.Rules.deserialize_from( - file=resources.ResourceAccessor().open(filepath, "rb") - ) + with resources.ResourceAccessor().open(filepath, "rb") as fp: + return yara_x.Rules.deserialize_from(file=fp) @staticmethod def from_file(filepath): - return yara_x.compile( - resources.ResourceAccessor().open(filepath, "rb").read().decode() - ) + with resources.ResourceAccessor().open(filepath, "rb") as fp: + return yara_x.compile(fp.read().decode()) YaraScanner = YaraXScanner if USE_YARA_X else YaraPythonScanner @@ -127,7 +127,7 @@ class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" _required_framework_version = (2, 0, 0) - _version = (1, 3, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -184,6 +184,11 @@ def get_yarascan_option_requirements( ), ] + @classmethod + def yara_returns_instances(cls) -> bool: + st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < (4, 3) + return st_object + @classmethod def process_yara_options(cls, config: Dict[str, Any]): rules = None From 1616a898ae5ddfe7369c0ea90306546abd4d929e Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 25 Jul 2024 08:37:27 +0200 Subject: [PATCH 006/250] restore comment --- volatility3/framework/plugins/yarascan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 7957aa0c53..3bf894d067 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -171,6 +171,8 @@ def get_yarascan_option_requirements( description="Yara rules (as a file)", optional=True, ), + # This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code + # As such, there's a separate option to run compiled files, as happened with yara-3.9 and later requirements.URIRequirement( name="yara_compiled_file", description="Yara compiled rules (as a file)", From c6727ffb701d31067e6617d49904614bedb63ac9 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 25 Jul 2024 08:43:21 +0200 Subject: [PATCH 007/250] fix f-string for previous python release --- volatility3/framework/plugins/yarascan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 3bf894d067..8d2f242d9e 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -204,11 +204,11 @@ def process_yara_options(cls, config: Dict[str, Any]): rule += " wide ascii" rules = YaraScanner.get_rule(rule) elif config.get("yara_file") is not None: - vollog.debug(f"Plain file: {config["yara_file"]} - yara-x: {USE_YARA_X}") + vollog.debug(f"Plain file: {config['yara_file']} - yara-x: {USE_YARA_X}") rules = YaraScanner.from_file(config["yara_file"]) elif config.get("yara_compiled_file") is not None: vollog.debug( - f"Compiled file: {config["yara_compiled_file"]} - yara-x: {USE_YARA_X}" + f"Compiled file: {config['yara_compiled_file']} - yara-x: {USE_YARA_X}" ) rules = YaraScanner.from_compiled_file(config["yara_compiled_file"]) else: From dc8dc9b078d42e41698ff93f2e65c5ee99aa2172 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Thu, 25 Jul 2024 14:37:18 +0200 Subject: [PATCH 008/250] fix yara depending plugins --- .../framework/plugins/linux/vmayarascan.py | 4 +- .../framework/plugins/windows/mftscan.py | 6 +-- .../framework/plugins/windows/vadyarascan.py | 49 +++++++++++++------ volatility3/framework/plugins/yarascan.py | 24 ++++----- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index eda0d7dcaf..6d055707ce 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -31,10 +31,10 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.PluginRequirement( - name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0) + name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) ), requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) ), requirements.ModuleRequirement( name="kernel", diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 1b1a1b45e5..9e65853450 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -29,7 +29,7 @@ def get_requirements(cls): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) ), ] @@ -38,7 +38,7 @@ def _generator(self): # Yara Rule to scan for MFT Header Signatures rules = yarascan.YaraScan.process_yara_options( - {"yara_rules": "/FILE0|FILE\\*|BAAD/"} + {"yara_string": "/FILE0|FILE\\*|BAAD/"} ) # Read in the Symbol File @@ -197,7 +197,7 @@ def _generator(self): # Yara Rule to scan for MFT Header Signatures rules = yarascan.YaraScan.process_yara_options( - {"yara_rules": "/FILE0|FILE\\*|BAAD/"} + {"yara_string": "/FILE0|FILE\\*|BAAD/"} ) # Read in the Symbol File diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index dc318dd934..4a84a12854 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -33,7 +33,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.PluginRequirement( - name="yarascan", plugin=yarascan.YaraScan, version=(1, 3, 0) + name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) ), requirements.ListRequirement( name="pid", @@ -73,26 +73,43 @@ def _generator(self): ) continue - for match in rules.match(data=layer.read(start, size, True)): - if yarascan.YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: + if not yarascan.YaraScan._yara_x: + for match in rules.match(data=layer.read(start, size, True)): + if yarascan.YaraScan.yara_returns_instances(): + for match_string in match.strings: + for instance in match_string.instances: + yield 0, ( + format_hints.Hex(instance.offset + start), + task.UniqueProcessId, + match.rule, + match_string.identifier, + instance.matched_data, + ) + else: + for offset, name, value in match.strings: yield 0, ( - format_hints.Hex(instance.offset + start), + format_hints.Hex(offset + start), task.UniqueProcessId, match.rule, + name, + value, + ) + else: + data = layer.read(start, size, True) + results = rules.scan(data) + for match in results.matching_rules: + for match_string in match.patterns: + for instance in match_string.matches: + yield 0, ( + format_hints.Hex(instance.offset + start), + task.UniqueProcessId, + f"{match.namespace}.{match.identifier}", match_string.identifier, - instance.matched_data, + data[ + instance.offset : instance.offset + + instance.length + ], ) - else: - for offset, name, value in match.strings: - yield 0, ( - format_hints.Hex(offset + start), - task.UniqueProcessId, - match.rule, - name, - value, - ) @staticmethod def get_vad_maps( diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 8d2f242d9e..6a4dd9251c 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -36,8 +36,8 @@ raise -class YaraPythonScanner(interfaces.layers.ScannerInterface): - _version = (2, 0, 0) +class BaseYaraScanner(interfaces.layers.ScannerInterface): + _version = (2, 1, 0) # yara.Rules isn't exposed, so we can't type this properly def __init__(self, rules) -> None: @@ -45,6 +45,11 @@ def __init__(self, rules) -> None: if rules is None: raise ValueError("No rules provided to YaraScanner") self._rules = rules + + +class YaraPythonScanner(BaseYaraScanner): + def __init__(self, rules) -> None: + super().__init__(rules) self.st_object = not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3) def __call__( @@ -81,16 +86,7 @@ def from_file(filepath): return yara.compile(file=fp) -class YaraXScanner(interfaces.layers.ScannerInterface): - _version = (2, 0, 0) - - # yara.Rules isn't exposed, so we can't type this properly - def __init__(self, rules) -> None: - super().__init__() - if rules is None: - raise ValueError("No rules provided to YaraScanner") - self._rules = rules - +class YaraXScanner(BaseYaraScanner): def __call__( self, data: bytes, data_offset: int ) -> Iterable[Tuple[int, str, str, bytes]]: @@ -128,6 +124,7 @@ class YaraScan(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (2, 0, 0) + _yara_x = USE_YARA_X @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -188,8 +185,7 @@ def get_yarascan_option_requirements( @classmethod def yara_returns_instances(cls) -> bool: - st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < (4, 3) - return st_object + return not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3) @classmethod def process_yara_options(cls, config: Dict[str, Any]): From 650dd06245918f1b14d8477eff85a743c9e42c4f Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:03:59 +0200 Subject: [PATCH 009/250] Modifications following the review --- volatility3/framework/plugins/linux/lsof.py | 74 +++++++++++-------- .../framework/symbols/linux/__init__.py | 53 +++++++------ 2 files changed, 71 insertions(+), 56 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index f9aeafe146..3bbc855f97 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -1,4 +1,4 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """A module containing a collection of plugins that produce data typically @@ -12,16 +12,17 @@ from volatility3.framework.objects import utility from volatility3.framework.symbols import linux from volatility3.plugins.linux import pslist +from volatility3.plugins import timeliner vollog = logging.getLogger(__name__) -class Lsof(plugins.PluginInterface): +class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -46,7 +47,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] @classmethod - def list_fds( + def list_fds_and_inodes( cls, context: interfaces.context.ContextInterface, symbol_table: str, @@ -67,27 +68,38 @@ def list_fds( ) for fd_fields in fd_generator: - yield pid, task_comm, task, fd_fields + fd_num, filp, full_path = fd_fields + inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) + try: + inode_num, file_size, imode, ctime, mtime, atime = next( + inode_metadata + ) + except Exception as e: + vollog.warning( + f"Can't get inode metadata for file descriptor {fd_num}: {e}" + ) + continue + yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) - fds_generator = self.list_fds( + fds_generator = self.list_fds_and_inodes( self.context, symbol_table, filter_func=filter_func ) - - for pid, task_comm, _task, fd_fields in fds_generator: - ( - fd_num, - _filp, - full_path, - inode_num, - imode, - ctime, - mtime, - atime, - file_size, - ) = fd_fields - + for ( + pid, + task_comm, + task, + fd_num, + filp, + full_path, + inode_num, + imode, + ctime, + mtime, + atime, + file_size, + ) in fds_generator: fields = ( pid, task_comm, @@ -113,22 +125,20 @@ def run(self): ("Path", str), ("Inode", int), ("Mode", str), - ("LastChange", datetime.datetime), - ("LastModify", datetime.datetime), - ("LastAccessed", datetime.datetime), + ("Changed", datetime.datetime), + ("Modified", datetime.datetime), + ("Accessed", datetime.datetime), ("Size", int), ] return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) def generate_timeline(self): + pids = self.config.get("pid", None) + symbol_table = self.config["kernel"] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - for row in self._generator( - pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=filter_func - ) - ): + for row in self._generator(pids, symbol_table): _depth, row_data = row - description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[4]}"' - yield description, timeliner.TimeLinerType.CHANGED, row_data[5] - yield description, timeliner.TimeLinerType.MODIFIED, row_data[6] - yield description, timeliner.TimeLinerType.ACCESSED, row_data[7] + description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' + yield description, timeliner.TimeLinerType.CHANGED, row_data[6] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[7] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[8] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index b9321f3693..2b97bc5f4c 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,9 +1,8 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import stat, datetime from typing import Iterator, List, Tuple, Optional, Union - +import logging, datetime, stat from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility @@ -62,7 +61,7 @@ def __init__(self, *args, **kwargs) -> None: class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 1, 0) + _version = (2, 2, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -266,26 +265,32 @@ def files_descriptors_for_process( for fd_num, filp in enumerate(fds): if filp != 0: full_path = LinuxUtilities.path_for_file(context, task, filp) - dentry = filp.get_dentry() - if dentry != 0: - inode_object = dentry.d_inode - inode_num = inode_object.i_ino - file_size = inode_object.i_size # file size in bytes - imode = stat.filemode( - inode_object.i_mode - ) # file type & Permissions - - # Timestamps - ctime = datetime.datetime.fromtimestamp( - inode_object.i_ctime.tv_sec - ) # last change time - mtime = datetime.datetime.fromtimestamp( - inode_object.i_mtime.tv_sec - ) # last modify time - atime = datetime.datetime.fromtimestamp( - inode_object.i_atime.tv_sec - ) # last access time - yield fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size + + yield fd_num, filp, full_path + + @classmethod + def get_inode_metadata(cls, context: interfaces.context.ContextInterface, filp): + """ + A helper function that gets the inodes metadata from a file descriptor + """ + dentry = filp.get_dentry() + if dentry != 0: + inode_object = dentry.d_inode + inode_num = inode_object.i_ino + file_size = inode_object.i_size # file size in bytes + imode = stat.filemode(inode_object.i_mode) # file type & Permissions + + # Timestamps + ctime = datetime.datetime.fromtimestamp( + inode_object.i_ctime.tv_sec + ) # last change time + mtime = datetime.datetime.fromtimestamp( + inode_object.i_mtime.tv_sec + ) # last modify time + atime = datetime.datetime.fromtimestamp( + inode_object.i_atime.tv_sec + ) # last access time + yield inode_num, file_size, imode, ctime, mtime, atime @classmethod def mask_mods_list( From 7024588076adf95c2d6667c851cddf87e8c68555 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:09:52 +0200 Subject: [PATCH 010/250] Code clean --- volatility3/framework/plugins/linux/lsof.py | 1 - volatility3/framework/symbols/linux/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 3bbc855f97..98a215ecfc 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -135,7 +135,6 @@ def run(self): def generate_timeline(self): pids = self.config.get("pid", None) symbol_table = self.config["kernel"] - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for row in self._generator(pids, symbol_table): _depth, row_data = row description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 2b97bc5f4c..1b3f75e984 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # from typing import Iterator, List, Tuple, Optional, Union -import logging, datetime, stat +import datetime, stat from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility From 815252c9ba31913d22e8836183828059217c4e9d Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Sat, 27 Jul 2024 16:35:22 +0200 Subject: [PATCH 011/250] Adding watchdogs --- volatility3/framework/plugins/linux/lsof.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 98a215ecfc..c1de48c1a1 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -78,7 +78,13 @@ def list_fds_and_inodes( vollog.warning( f"Can't get inode metadata for file descriptor {fd_num}: {e}" ) - continue + # Yield NotAvailableValue for each field in case of an exception + inode_num = renderers.NotAvailableValue() + file_size = renderers.NotAvailableValue() + imode = renderers.NotAvailableValue() + ctime = renderers.NotAvailableValue() + mtime = renderers.NotAvailableValue() + atime = renderers.NotAvailableValue() yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): From 59fec176aa5377e7324c6996770d27a46fdebd9a Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 29 Jul 2024 18:49:48 -0500 Subject: [PATCH 012/250] Fix bugs in thrdscan and threads. Add orphan kernel threads plugin --- .../plugins/windows/orphan_kernel_threads.py | 95 +++++++++++++++++++ .../framework/plugins/windows/thrdscan.py | 2 +- .../framework/plugins/windows/threads.py | 12 +-- 3 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 volatility3/framework/plugins/windows/orphan_kernel_threads.py diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py new file mode 100644 index 0000000000..013ba98ff6 --- /dev/null +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -0,0 +1,95 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List, Generator + +from volatility3.framework import interfaces, symbols +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import thrdscan, ssdt + +vollog = logging.getLogger(__name__) + + +class Threads(thrdscan.ThrdScan): + """Lists process threads""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.implementation = self.list_orphan_kernel_threads + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="thrdscan", plugin=thrdscan.ThrdScan, version=(1, 1, 0) + ), + requirements.PluginRequirement( + name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + ), + ] + + @classmethod + def list_orphan_kernel_threads( + cls, + context: interfaces.context.ContextInterface, + module_name: str, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Yields thread objects of kernel threads that do not map to a module + + Args: + kernel + + Returns: + A generator of thread objects of orphaned threads + """ + module = context.modules[module_name] + layer_name = module.layer_name + symbol_table = module.symbol_table_name + + collection = ssdt.SSDT.build_module_collection( + context, layer_name, symbol_table + ) + + # used to filter out smeared pointers + if symbols.symbol_table_is_64bit(context, symbol_table): + kernel_start = 0xFFFFF80000000000 + else: + kernel_start = 0x80000000 + + for thread in thrdscan.ThrdScan.scan_threads(context, module_name): + # we don't want smeared or terminated threads + try: + proc = thread.owning_process() + except AttributeError: + continue + + # we only care about kernel threads, 4 = System + # previous methods for determining if a thread was a kernel thread + # such as bit fields and flags are not stable in Win10+ + # so we check if the thread is from the kernel itself or one its child + # kernel processes (MemCompression, Regsitry, ...) + if proc.UniqueProcessId != 4 and proc.InheritedFromUniqueProcessId != 4: + continue + + if thread.StartAddress < kernel_start: + continue + + module_symbols = list( + collection.get_module_symbols_by_absolute_location(thread.StartAddress) + ) + + # alert on threads that do not map to a module + if not module_symbols: + yield thread + diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 6e664e0526..ad885e8e9c 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -22,8 +22,8 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) _version = (1, 1, 0) def __init__(self, *args, **kwargs): - self.implementation = self.scan_threads super().__init__(*args, **kwargs) + self.implementation = self.scan_threads @classmethod def get_requirements(cls): diff --git a/volatility3/framework/plugins/windows/threads.py b/volatility3/framework/plugins/windows/threads.py index ae70e717b0..98a3169a5b 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): + super().__init__(*args, **kwargs) self.implementation = self.list_process_threads - super().__init__() @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -50,7 +50,6 @@ def list_threads( Args: proc: _EPROCESS object from which to list the VADs - filter_func: Function to take a virtual address descriptor value and return True if it should be filtered out Returns: A list of threads based on the process and filtered based on the filter function @@ -64,22 +63,19 @@ def list_threads( seen.add(thread.vol.offset) yield thread - @classmethod - def filter_func(cls, config: interfaces.configuration.HierarchicalDict) -> Callable: - return pslist.PsList.create_pid_filter(config.get("pid", None)) - @classmethod def list_process_threads( cls, context: interfaces.context.ContextInterface, module_name: str, - filter_func: Callable, ) -> Iterable[interfaces.objects.ObjectInterface]: """Runs through all processes and lists threads for each process""" module = context.modules[module_name] layer_name = module.layer_name symbol_table_name = module.symbol_table_name + filter_func = pslist.PsList.create_pid_filter(context.config.get("pid", None)) + for proc in pslist.PsList.list_processes( context=context, layer_name=layer_name, From 896b40bd22028f8e68d954a9f9080d564c6982e2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 29 Jul 2024 18:50:55 -0500 Subject: [PATCH 013/250] Fix bugs in thrdscan and threads. Add orphan kernel threads plugin --- volatility3/framework/plugins/windows/orphan_kernel_threads.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index 013ba98ff6..1cdb1dcdfa 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -92,4 +92,3 @@ def list_orphan_kernel_threads( # alert on threads that do not map to a module if not module_symbols: yield thread - From 90b8b5340f9bc886e9b45ecadd80b7d86b8384f3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 1 Aug 2024 11:46:10 +1000 Subject: [PATCH 014/250] 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 c4e2587f49..03353135d0 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -29,12 +29,18 @@ def __init__(self, *args, **kwargs) -> None: 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 e7c6b66d7d..be31e298cb 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 @@ def get_capabilities(self) -> int: ) 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 dbc837474c9c1d6096abd7cd27496677f0a36e7b Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 1 Aug 2024 14:23:22 -0500 Subject: [PATCH 015/250] #816 - initial support for Windows Windows Server 2022 (10.0.20348) --- .../framework/plugins/windows/consoles.py | 786 ++++++++++++++++++ .../consoles-win10-20348-2461-x64.json | 595 +++++++++++++ .../consoles/consoles-win10-20348-x64.json | 595 +++++++++++++ .../symbols/windows/extensions/consoles.py | 303 +++++++ 4 files changed, 2279 insertions(+) create mode 100644 volatility3/framework/plugins/windows/consoles.py create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json create mode 100644 volatility3/framework/symbols/windows/extensions/consoles.py diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py new file mode 100644 index 0000000000..f6bbb3ae74 --- /dev/null +++ b/volatility3/framework/plugins/windows/consoles.py @@ -0,0 +1,786 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# This module attempts to locate windows console histories. + +import logging +import os +import struct +from typing import Tuple, Generator, Set, Dict, Any, Type + +from volatility3.framework import interfaces, symbols, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import scanners +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows import pdbutil, versions +from volatility3.framework.symbols.windows.extensions import pe, consoles +from volatility3.plugins.windows import pslist, vadinfo, info, verinfo +from volatility3.plugins.windows.registry import hivelist + + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +vollog = logging.getLogger(__name__) + + +class Consoles(interfaces.plugins.PluginInterface): + """Looks for Windows console buffers""" + + _required_framework_version = (2, 4, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="no_registry", + description="Don't search the registry for possible values of CommandHistorySize and HistoryBufferMax", + optional=True, + default=False, + ), + requirements.ListRequirement( + name="max_history", + element_type=int, + description="CommandHistorySize values to search for.", + optional=True, + default=[50], + ), + requirements.ListRequirement( + name="max_buffers", + element_type=int, + description="HistoryBufferMax values to search for.", + optional=True, + default=[4], + ), + ] + + @classmethod + def find_conhost_proc( + cls, proc_list: Generator[interfaces.objects.ObjectInterface, None, None] + ) -> Tuple[interfaces.context.ContextInterface, str]: + """ + Walks the process list and returns the conhost instances. + + Args: + proc_list: The process list generator + + Return: + The process object and layer name for conhost + """ + + for proc in proc_list: + try: + proc_id = proc.UniqueProcessId + proc_layer_name = proc.add_process_layer() + + yield proc, proc_layer_name + + except exceptions.InvalidAddressException as excp: + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) + + @classmethod + def find_conhostexe( + cls, conhost_proc: interfaces.context.ContextInterface + ) -> Tuple[int, int]: + """ + Finds the base address of conhost.exe + + Args: + conhost_proc: the process object for conhost.exe + + Returns: + A tuple of: + conhostexe_base: the base address of conhost.exe + conhostexe_size: the size of the VAD for conhost.exe + """ + for vad in conhost_proc.get_vad_root().traverse(): + filename = vad.get_file_name() + + if isinstance(filename, str) and filename.lower().endswith("conhost.exe"): + base = vad.get_start() + return base, vad.get_size() + + return None, None + + @classmethod + def determine_conhost_version( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbol_table: str, + ) -> Tuple[str, Type]: + """Tries to determine which symbol filename to use for the image's console information. This is similar to the + netstat plugin. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + layer_name: The name of the layer on which to operate + nt_symbol_table: The name of the table containing the kernel symbols + + Returns: + The filename of the symbol table to use and the associated class types. + """ + + is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) + + is_18363_or_later = versions.is_win10_18363_or_later( + context=context, symbol_table=nt_symbol_table + ) + + if is_64bit: + arch = "x64" + else: + arch = "x86" + + vers = info.Info.get_version_structure(context, layer_name, nt_symbol_table) + + kuser = info.Info.get_kuser_structure(context, layer_name, nt_symbol_table) + + try: + vers_minor_version = int(vers.MinorVersion) + nt_major_version = int(kuser.NtMajorVersion) + nt_minor_version = int(kuser.NtMinorVersion) + except ValueError: + # vers struct exists, but is not an int anymore? + raise NotImplementedError( + "Kernel Debug Structure version format not supported!" + ) + except: + # unsure what to raise here. Also, it might be useful to add some kind of fallback, + # either to a user-provided version or to another method to determine tcpip.sys's version + raise exceptions.VolatilityException( + "Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!" + ) + + vollog.debug( + "Determined OS Version: {}.{} {}.{}".format( + kuser.NtMajorVersion, + kuser.NtMinorVersion, + vers.MajorVersion, + vers.MinorVersion, + ) + ) + + if nt_major_version == 10 and arch == "x64": + # win10 x64 has an additional class type we have to include. + class_types = consoles.win10_x64_class_types + else: + # default to general class types + class_types = consoles.class_types + + # these versions are listed explicitly because symbol files differ based on + # version *and* architecture. this is currently the clearest way to show + # the differences, even if it introduces a fair bit of redundancy. + # furthermore, it is easy to append new versions. + if arch == "x86": + version_dict = {} + else: + version_dict = { + (10, 0, 20348, 1): "consoles-win10-20348-x64", + (10, 0, 20348, 1970): "consoles-win10-20348-1970-x64", + (10, 0, 20348, 2461): "consoles-win10-20348-2461-x64", + (10, 0, 20348, 2520): "consoles-win10-20348-2461-x64", + } + + # we do not need to check for conhost's specific FileVersion in every case + conhost_mod_version = 0 # keep it 0 as a default + + # special use cases + + # Win10_18363 is not recognized by windows.info as 18363 + # because all kernel file headers and debug structures report 18363 as + # "10.0.18362.1198" with the last part being incremented. However, we can use + # os_distinguisher to differentiate between 18362 and 18363 + if vers_minor_version == 18362 and is_18363_or_later: + vollog.debug( + "Detected 18363 data structures: working with 18363 symbol table." + ) + vers_minor_version = 18363 + + # we need to define additional version numbers (which are then found via conhost.exe's FileVersion header) in case there is + # ambiguity _within_ an OS version. If such a version number (last number of the tuple) is defined for the current OS + # we need to inspect conhost.exe's headers to see if we can grab the precise version + if [ + (a, b, c, d) + for a, b, c, d in version_dict + if (a, b, c) == (nt_major_version, nt_minor_version, vers_minor_version) + and d != 0 + ]: + vollog.debug( + "Requiring further version inspection due to OS version by checking conhost.exe's FileVersion header" + ) + # the following is IntelLayer specific and might need to be adapted to other architectures. + physical_layer_name = context.layers[layer_name].config.get( + "memory_layer", None + ) + if physical_layer_name: + ver = verinfo.VerInfo.find_version_info( + context, physical_layer_name, "CONHOST.EXE" + ) + + if ver: + conhost_mod_version = ver[3] + vollog.debug( + "Determined conhost.exe's FileVersion: {}".format( + conhost_mod_version + ) + ) + else: + vollog.debug("Could not determine conhost.exe's FileVersion.") + else: + vollog.debug( + "Unable to retrieve physical memory layer, skipping FileVersion check." + ) + + # when determining the symbol file we have to consider the following cases: + # the determined version's symbol file is found by intermed.create -> proceed + # the determined version's symbol file is not found by intermed -> intermed will throw an exc and abort + # the determined version has no mapped symbol file -> if win10 use latest, otherwise throw exc + # windows version cannot be determined -> throw exc + + filename = version_dict.get( + ( + nt_major_version, + nt_minor_version, + vers_minor_version, + conhost_mod_version, + ) + ) + + if not filename: + # no match on filename means that we possibly have a version newer than those listed here. + # try to grab the latest supported version of the current image NT version. If that symbol + # version does not work, support has to be added manually. + current_versions = [ + (nt_maj, nt_min, vers_min, tcpip_ver) + for nt_maj, nt_min, vers_min, tcpip_ver in version_dict + if nt_maj == nt_major_version + and nt_min == nt_minor_version + and tcpip_ver <= conhost_mod_version + ] + current_versions.sort() + + if current_versions: + latest_version = current_versions[-1] + + filename = version_dict.get(latest_version) + + vollog.debug( + f"Unable to find exact matching symbol file, going with latest: {filename}" + ) + + else: + raise NotImplementedError( + "This version of Windows is not supported: {}.{} {}.{}!".format( + nt_major_version, + nt_minor_version, + vers.MajorVersion, + vers_minor_version, + ) + ) + + vollog.debug(f"Determined symbol filename: {filename}") + + return filename, class_types + + @classmethod + def create_conhost_symbol_table( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbol_table: str, + config_path: str, + ) -> str: + """Creates a symbol table for TCP Listeners and TCP/UDP Endpoints. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + layer_name: The name of the layer on which to operate + nt_symbol_table: The name of the table containing the kernel symbols + config_path: The config path where to find symbol files + + Returns: + The name of the constructed symbol table + """ + table_mapping = {"nt_symbols": nt_symbol_table} + + symbol_filename, class_types = cls.determine_conhost_version( + context, + layer_name, + nt_symbol_table, + ) + + vollog.debug(f"Using symbol file '{symbol_filename}' and types {class_types}") + + return intermed.IntermediateSymbolTable.create( + context, + config_path, + os.path.join("windows", "consoles"), + symbol_filename, + class_types=class_types, + table_mapping=table_mapping, + ) + + @classmethod + def get_console_info( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + kernel_table_name: str, + config_path: str, + procs: Generator[interfaces.objects.ObjectInterface, None, None], + max_history: Set[int], + max_buffers: Set[int], + ) -> Tuple[ + interfaces.context.ContextInterface, + interfaces.context.ContextInterface, + Dict[str, Any], + ]: + """Extracts the cmdline from PEB + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_layer_name: The name of the layer on which to operate + kernel_table_name: The name of the table containing the kernel symbols + config_path: The config path where to find symbol files + procs: list of process objects + max_history: an initial set of CommandHistorySize values + max_buffers: an initial list of HistoryBufferMax values + + Returns: + The conhost process object, the console information structure, a dictionary of properties for + that console information structure. + """ + + conhost_symbol_table = cls.create_conhost_symbol_table( + context, kernel_layer_name, kernel_table_name, config_path + ) + + for conhost_proc, proc_layer_name in cls.find_conhost_proc(procs): + if not conhost_proc: + vollog.info( + "Unable to find a valid conhost.exe process in the process list. Analysis cannot proceed." + ) + continue + vollog.debug( + f"Found conhost process {conhost_proc} with pid {conhost_proc.UniqueProcessId}" + ) + + conhostexe_base, conhostexe_size = cls.find_conhostexe(conhost_proc) + if not conhostexe_base: + vollog.info( + "Unable to find the location of conhost.exe. Analysis cannot proceed." + ) + continue + vollog.debug(f"Found conhost.exe base at {conhostexe_base:#x}") + + proc_layer = context.layers[proc_layer_name] + + conhost_module = context.module( + conhost_symbol_table, proc_layer_name, offset=conhostexe_base + ) + + # scan for potential _CONSOLE_INFORMATION structures by using the CommandHistorySize + for max_history_value in max_history: + max_history_bytes = struct.pack("H", max_history_value) + vollog.debug( + f"Scanning for CommandHistorySize value: {max_history_bytes}" + ) + for address in proc_layer.scan( + context, + scanners.BytesScanner(max_history_bytes), + sections=[(conhostexe_base, conhostexe_size)], + ): + + console_properties = [] + + try: + console_info = conhost_module.object( + "_CONSOLE_INFORMATION", + offset=address + - conhost_module.get_type( + "_CONSOLE_INFORMATION" + ).relative_child_offset("CommandHistorySize"), + absolute=True, + ) + + if not any( + [ + console_info.is_valid(max_buffer) + for max_buffer in max_buffers + ] + ): + continue + + vollog.debug( + f"Getting Console Information properties for {console_info}" + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.ScreenX", + "address": console_info.ScreenX.vol.offset, + "data": console_info.ScreenX, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.ScreenY", + "address": console_info.ScreenY.vol.offset, + "data": console_info.ScreenY, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.CommandHistorySize", + "address": console_info.CommandHistorySize.vol.offset, + "data": console_info.CommandHistorySize, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.HistoryBufferCount", + "address": console_info.HistoryBufferCount.vol.offset, + "data": console_info.HistoryBufferCount, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.HistoryBufferMax", + "address": console_info.HistoryBufferMax.vol.offset, + "data": console_info.HistoryBufferMax, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.Title", + "address": console_info.Title.vol.offset, + "data": console_info.get_title(), + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.OriginalTitle", + "address": console_info.OriginalTitle.vol.offset, + "data": console_info.get_original_title(), + } + ) + + vollog.debug( + f"Getting ConsoleProcessList entries for {console_info.ConsoleProcessList}" + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.ProcessCount", + "address": console_info.ProcessCount.vol.offset, + "data": console_info.ProcessCount, + } + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.ConsoleProcessList", + "address": console_info.ConsoleProcessList.vol.offset, + "data": "", + } + ) + for index, attached_proc in enumerate( + console_info.get_processes() + ): + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}", + "address": attached_proc.ConsoleProcess.dereference().vol.offset, + "data": "", + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessId", + "address": attached_proc.ConsoleProcess.ProcessId.vol.offset, + "data": attached_proc.ConsoleProcess.ProcessId, + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessHandle", + "address": attached_proc.ConsoleProcess.ProcessHandle.vol.offset, + "data": hex( + attached_proc.ConsoleProcess.ProcessHandle + ), + } + ) + + vollog.debug( + f"Getting HistoryList entries for {console_info.HistoryList}" + ) + console_properties.append( + { + "name": "_CONSOLE_INFORMATION.HistoryList", + "address": console_info.HistoryList.vol.offset, + "data": "", + } + ) + for index, command_history in enumerate( + console_info.get_histories() + ): + try: + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}", + "address": command_history.vol.offset, + "data": "", + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Application", + "address": command_history.Application.vol.offset, + "data": command_history.get_application(), + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_ProcessHandle", + "address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset, + "data": hex( + command_history.ConsoleProcessHandle.ProcessHandle + ), + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_CommandCount", + "address": None, + "data": command_history.CommandCount, + } + ) + for ( + cmd_index, + bucket_cmd, + ) in command_history.get_commands(): + try: + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Command_{cmd_index}", + "address": bucket_cmd.vol.offset, + "data": bucket_cmd.get_command(), + } + ) + except Exception as e: + vollog.debug( + f"reading {bucket_cmd} encountered exception {e}" + ) + except Exception as e: + vollog.debug( + f"reading {command_history} encountered exception {e}" + ) + + vollog.debug(f"Getting ScreenBuffer entries for {console_info}") + for screen_index, screen_info in enumerate( + console_info.get_screens() + ): + try: + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}", + "address": screen_info, + "data": "", + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenX", + "address": None, + "data": screen_info.ScreenX, + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenY", + "address": None, + "data": screen_info.ScreenY, + } + ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.Dump", + "address": None, + "data": "\n".join(screen_info.get_buffer()), + } + ) + except Exception as e: + vollog.debug( + f"reading {screen_info} encountered exception {e}" + ) + + except exceptions.PagedInvalidAddressException as exp: + vollog.debug( + f"Required memory at {exp.invalid_address:#x} is not valid" + ) + + yield conhost_proc, console_info, console_properties + + @classmethod + def get_console_settings_from_registry( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel_layer_name: str, + kernel_symbol_table_name: str, + max_history: Set[int], + max_buffers: Set[int], + ) -> Tuple[Set[int], Set[int]]: + """ + Walks the Registry user hives and extracts any CommandHistorySize and HistoryBufferMax values + for scanning + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + config_path: The config path where to find symbol files + kernel_layer_name: The name of the layer on which to operate + kernel_symbol_table_name: The name of the table containing the kernel symbols + max_history: an initial set of CommandHistorySize values + max_buffers: an initial list of HistoryBufferMax values + + Returns: + The updated max_history and max_buffers sets. + """ + vollog.debug( + f"Possible CommandHistorySize values before checking Registry: {max_history}" + ) + vollog.debug( + f"Possible HistoryBufferMax values before checking Registry: {max_buffers}" + ) + + for hive in hivelist.HiveList.list_hives( + context=context, + base_config_path=config_path, + layer_name=kernel_layer_name, + symbol_table=kernel_symbol_table_name, + hive_offsets=None, + ): + try: + for value in hive.get_key("Console").get_values(): + val_name = str(value.get_name()) + if val_name == "HistoryBufferSize": + max_history.add(value.decode_data()) + elif val_name == "NumberOfHistoryBuffers": + max_buffers.add(value.decode_data()) + except: + continue + + return max_history, max_buffers + + def _generator( + self, procs: Generator[interfaces.objects.ObjectInterface, None, None] + ): + """ + Generates the console information to use in rendering + + Args: + procs: the process list filtered to conhost.exe instances + """ + + kernel = self.context.modules[self.config["kernel"]] + + max_history = set(self.config.get("max_history", [50])) + max_buffers = set(self.config.get("max_buffers", [4])) + no_registry = self.config.get("no_registry") + + if no_registry is False: + max_history, max_buffers = self.get_console_settings_from_registry( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + max_history, + max_buffers, + ) + + vollog.debug(f"Possible CommandHistorySize values: {max_history}") + vollog.debug(f"Possible HistoryBufferMax values: {max_buffers}") + + for proc, console_info, console_properties in self.get_console_info( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + self.config_path, + procs, + max_history, + max_buffers, + ): + process_name = utility.array_to_string(proc.ImageFileName) + + if console_info and console_properties: + for console_property in console_properties: + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(console_info.vol.offset), + console_property["name"], + ( + renderers.NotApplicableValue() + if console_property["address"] is None + else format_hints.Hex(console_property["address"]) + ), + str(console_property["data"]), + ), + ) + + def _conhost_proc_filter(self, proc): + """ + Used to filter to only conhost.exe processes + """ + process_name = utility.array_to_string(proc.ImageFileName) + + return process_name != "conhost.exe" + + def run(self): + kernel = self.context.modules[self.config["kernel"]] + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("ConsoleInfo", format_hints.Hex), + ("Property", str), + ("Address", format_hints.Hex), + ("Data", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=self._conhost_proc_filter, + ) + ), + ) 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 new file mode 100644 index 0000000000..3ae41fdd79 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-2461-x64.json @@ -0,0 +1,595 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 24 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 26 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 136 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 140 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1616 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1552 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1680 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1296 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1272 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1280 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 9176 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 9184 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 9232 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 16 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 40 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 48 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 54 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 96 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json new file mode 100644 index 0000000000..b74862598f --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json @@ -0,0 +1,595 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 24 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 26 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 136 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 140 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1616 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1552 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1680 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1296 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1272 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1280 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -288 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -280 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -376 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 16 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 40 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 48 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 54 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 96 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py new file mode 100644 index 0000000000..2db1f4cad9 --- /dev/null +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -0,0 +1,303 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import objects +from volatility3.framework import constants + + +class ROW(objects.StructType): + """A Row Structure.""" + + def _valid_dbcs(self, c): + # TODO this need more research and testing + # https://github.com/search?q=repo%3Amicrosoft%2Fterminal+DbcsAttr&type=code + valid = c in ( + 0x0, + 0x1, + 0x2, + 0x20, + 0x28, + 0x30, + 0x48, + 0x60, + 0x80, + 0xF8, + 0xF0, + 0xA0, + ) + # if not valid: + # print("Bad Dbcs Attribute {}".format(hex(c))) + return valid + + def get_text(self, truncate=True): + """A convenience method to extract the text from the _ROW. The _ROW + contains a pointer CharRow to an array of CharRowCell objects. Each + CharRowCell contains the wide character and an attribute. Enumerating + self.CharRow.Chars and casting each character to unicode takes too long, + so this reads the whole row into a buffer, then extracts the text characters.""" + + layer = self._context.layers[self.vol.layer_name] + offset = self.CharRow.Chars.vol.offset + length = self.RowLength * 3 + char_row = layer.read(offset, length) + line = "" + try: + if char_row: + line = "".join( + # "{} {} =".format(char_row[i:i + 2].decode('utf-16le', errors='replace'), char_row[i+2]) if self._valid_dbcs(char_row[i+2]) else "" + ( + char_row[i : i + 2].decode("utf-16le", errors="replace") + if self._valid_dbcs(char_row[i + 2]) + else "" + ) + for i in range(0, len(char_row), 3) + ) + except Exception as e: + print(e) + line = "" + + if truncate: + return line.rstrip() + else: + return line + + +class SCREEN_INFORMATION(objects.StructType): + """A Screen Information Structure.""" + + @property + def ScreenX(self): + return self.TextBufferInfo.BufferRows.Rows[0].Row.RowLength2 + + @property + def ScreenY(self): + return self.TextBufferInfo.BufferCapacity + + def _truncate_rows(self, rows): + """To truncate empty rows at the end, walk the list + backwards and get the last non-empty row. Use that + row index to splice. Rows are created based on the + length given in the ROW structure, so empty rows will + be ''.""" + + non_empty_index = 0 + rows_traversed = False + + for index, row in enumerate(reversed(rows)): + # the string was created based on the length in the ROW structure so it shouldn't have any bad data + if len(row.rstrip()) > 0: + non_empty_index = index + break + rows_traversed = True + + if non_empty_index == 0 and rows_traversed: + rows = [] + else: + rows = rows[0 : len(rows) - non_empty_index] + + return rows + + def get_buffer(self, truncate_rows=True, truncate_lines=True): + """Get the screen buffer. + + The screen buffer is comprised of the screen's Y + coordinate which tells us the number of rows and + the X coordinate which tells us the width of each + row in characters. Windows 10 17763 changed from + a large text buffer to a grid of cells, with each + cell containing a single wide character in that + cell, stored in a CharRowCell object. + + @param truncate: True if the empty rows at the + end (i.e. bottom) of the screen buffer should be + supressed. + """ + rows = [] + + capacity = self.TextBufferInfo.BufferCapacity + start = self.TextBufferInfo.BufferStart + buffer_rows = self.TextBufferInfo.BufferRows.dereference() + buffer_rows.Rows.count = self.TextBufferInfo.BufferCapacity + + for i in range(capacity): + index = (start + i) % capacity + row = buffer_rows.Rows[index].Row + try: + text = row.get_text(truncate_lines) + rows.append(text) + except: + break + + if truncate_rows: + rows = self._truncate_rows(rows) + + if rows: + rows = ["=== Start of buffer ==="] + rows + ["=== End of buffer ==="] + else: + rows = ["=== No buffer data found ==="] + return rows + + +class CONSOLE_INFORMATION(objects.StructType): + """A Console Information Structure.""" + + @property + def ScreenBuffer(self): + return self.GetScreenBuffer + + def is_valid(self, max_buffers=4) -> bool: + """Determine if the structure is valid.""" + + # Last displayed must be between -1 and max + if self.HistoryBufferCount < 1 or self.HistoryBufferCount > max_buffers: + return False + + if not self.get_title() and not self.get_original_title(): + return False + + return True + + def get_screens(self): + """Generator for screens in the console. + + A console can have multiple screen buffers at a time, + but only the current/active one is displayed. + + Multiple screens are tracked using the singly-linked + list _SCREEN_INFORMATION.Next. + + See CreateConsoleScreenBuffer + """ + screens = [self.CurrentScreenBuffer] + + if self.ScreenBuffer not in screens: + screens.append(self.ScreenBuffer) + + seen = set() + + for screen in screens: + cur = screen + while cur and cur.vol.offset != 0 and cur.vol.offset not in seen: + cur.TextBufferInfo.BufferRows.Rows.count = ( + cur.TextBufferInfo.BufferCapacity + ) + yield cur + seen.add(cur.vol.offset) + cur = cur.Next + + def get_histories(self): + for cmd_hist in self.HistoryList.dereference().to_list( + f"{self.get_symbol_table_name()}{constants.BANG}_COMMAND_HISTORY", + "ListEntry", + ): + yield cmd_hist + + def get_processes(self): + for proc in self.ConsoleProcessList.dereference().to_list( + f"{self.get_symbol_table_name()}{constants.BANG}_CONSOLE_PROCESS_LIST", + "ListEntry", + ): + yield proc + + def get_title(self): + try: + return self.Title.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + except: + return "" + + def get_original_title(self): + try: + return self.OriginalTitle.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + except: + return "" + + +class COMMAND(objects.StructType): + """A Command Structure""" + + def get_command(self): + if self.Length < 8: + return self.Chars.cast( + "string", + encoding="utf-16", + errors="replace", + max_length=self.Length * 2, + ) + elif self.Length < 1024: + return self.Pointer.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + + +class COMMAND_HISTORY(objects.StructType): + """A Command History Structure.""" + + @property + def CommandCount(self): + command_type = self.get_symbol_table_name() + constants.BANG + "_COMMAND" + command_size = self._context.symbol_space.get_type(command_type).size + return int((self.CommandBucket.End - self.CommandBucket.Begin) / command_size) + + def get_application(self): + if self.Application.Length < 8: + return self.Application.Chars.cast( + "string", + encoding="utf-16", + errors="replace", + max_length=self.Application.Length * 2, + ) + elif self.Application.Length < 1024: + return self.Application.Pointer.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + + def scan_command_bucket(self, end=None): + """Brute force print all strings pointed to by the CommandBucket entries by + going to greater of EndCapacity or CommandCountMax*sizeof(_COMMAND)""" + + command_type = self.get_symbol_table_name() + constants.BANG + "_COMMAND" + command_history_size = self._context.symbol_space.get_type( + self.vol.type_name + ).size + command_size = self._context.symbol_space.get_type(command_type).size + if end is None: + end = max( + self.CommandBucket.EndCapacity, + self.CommandBucket.Begin + command_history_size * self.CommandCountMax, + ) + + for i, pointer in enumerate(range(self.CommandBucket.Begin, end, command_size)): + yield i, self._context.object(command_type, self.vol.layer_name, pointer) + + def get_commands(self): + """Generator for commands in the history buffer. + + The CommandBucket is an array of pointers to _COMMAND + structures. The array size is CommandCount. Once CommandCount + is reached, the oldest commands are cycled out and the + rest are coalesced. + """ + + for i, cmd in self.scan_command_bucket(self.CommandBucket.End): + yield i, cmd + + +win10_x64_class_types = { + "_ROW": ROW, + "_SCREEN_INFORMATION": SCREEN_INFORMATION, + "_CONSOLE_INFORMATION": CONSOLE_INFORMATION, + "_COMMAND_HISTORY": COMMAND_HISTORY, + "_COMMAND": COMMAND, +} +class_types = { + "_ROW": ROW, + "_SCREEN_INFORMATION": SCREEN_INFORMATION, + "_CONSOLE_INFORMATION": CONSOLE_INFORMATION, + "_COMMAND_HISTORY": COMMAND_HISTORY, + "_COMMAND": COMMAND, +} From fdb131f870f5a3ef97dfe5f33e3ee05cf17d83f2 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Thu, 1 Aug 2024 16:27:48 -0500 Subject: [PATCH 016/250] #816 - add cmdscan and bug fixes --- .../framework/plugins/windows/cmdscan.py | 327 ++++++++++++++++++ .../framework/plugins/windows/consoles.py | 2 +- .../consoles-win10-20348-2461-x64.json | 2 +- .../consoles/consoles-win10-20348-x64.json | 2 +- .../symbols/windows/extensions/consoles.py | 31 +- 5 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 volatility3/framework/plugins/windows/cmdscan.py diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py new file mode 100644 index 0000000000..86a34a4f8f --- /dev/null +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -0,0 +1,327 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# This module attempts to locate windows console histories. + +import logging +import struct +from typing import Tuple, Generator, Set, Dict, Any, List, Optional + +from volatility3.framework import interfaces +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import scanners +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, vadinfo, info, verinfo, consoles +from volatility3.plugins.windows.registry import hivelist + + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +vollog = logging.getLogger(__name__) + + +class CmdScan(interfaces.plugins.PluginInterface): + """Looks for Windows Command History lists""" + + _required_framework_version = (2, 4, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="no_registry", + description="Don't search the registry for possible values of CommandHistorySize", + optional=True, + default=False, + ), + requirements.ListRequirement( + name="max_history", + element_type=int, + description="CommandHistorySize values to search for.", + optional=True, + default=[50], + ), + ] + + @classmethod + def get_filtered_vads( + cls, conhost_proc: interfaces.context.ContextInterface, size_filter: Optional[int]=0x40000000 + ) -> List[Tuple[int, int]]: + """ + Returns vads of a process with smaller than size_filter + + Args: + conhost_proc: the process object for conhost.exe + + Returns: + A list of tuples of: + vad_base: the base address + vad_size: the size of the VAD + """ + vads = [] + for vad in conhost_proc.get_vad_root().traverse(): + base = vad.get_start() + if vad.get_size() < size_filter: + vads.append((base, vad.get_size())) + + return vads + + @classmethod + def get_command_history( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + kernel_table_name: str, + config_path: str, + procs: Generator[interfaces.objects.ObjectInterface, None, None], + max_history: Set[int], + ) -> Tuple[ + interfaces.context.ContextInterface, + interfaces.context.ContextInterface, + Dict[str, Any], + ]: + """Gets the list of commands from each Command History structure + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_layer_name: The name of the layer on which to operate + kernel_table_name: The name of the table containing the kernel symbols + config_path: The config path where to find symbol files + procs: list of process objects + max_history: an initial set of CommandHistorySize values + + Returns: + The conhost process object, the command history structure, a dictionary of properties for + that command historyn structure. + """ + + conhost_symbol_table = consoles.Consoles.create_conhost_symbol_table( + context, kernel_layer_name, kernel_table_name, config_path + ) + + for conhost_proc, proc_layer_name in consoles.Consoles.find_conhost_proc(procs): + if not conhost_proc: + vollog.info( + "Unable to find a valid conhost.exe process in the process list. Analysis cannot proceed." + ) + continue + vollog.debug( + f"Found conhost process {conhost_proc} with pid {conhost_proc.UniqueProcessId}" + ) + + conhostexe_base, conhostexe_size = consoles.Consoles.find_conhostexe(conhost_proc) + if not conhostexe_base: + vollog.info( + "Unable to find the location of conhost.exe. Analysis cannot proceed." + ) + continue + vollog.debug(f"Found conhost.exe base at {conhostexe_base:#x}") + + proc_layer = context.layers[proc_layer_name] + + conhost_module = context.module( + conhost_symbol_table, proc_layer_name, offset=conhostexe_base + ) + + sections = cls.get_filtered_vads(conhost_proc) + # scan for potential _COMMAND_HISTORY structures by using the CommandHistorySize + for max_history_value in max_history: + max_history_bytes = struct.pack("H", max_history_value) + vollog.debug( + f"Scanning for CommandHistorySize value: {max_history_bytes}" + ) + for address in proc_layer.scan( + context, + scanners.BytesScanner(max_history_bytes), + sections=sections, + ): + command_history_properties = [] + + try: + command_history = conhost_module.object( + "_COMMAND_HISTORY", + offset=address + - conhost_module.get_type( + "_COMMAND_HISTORY" + ).relative_child_offset("CommandCountMax"), + absolute=True, + ) + + if not command_history.is_valid(max_history_value): + continue + + vollog.debug( + f"Getting Command History properties for {command_history}" + ) + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.Application", + "address": command_history.Application.vol.offset, + "data": command_history.get_application(), + } + ) + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.ProcessHandle", + "address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset, + "data": hex( + command_history.ConsoleProcessHandle.ProcessHandle + ), + } + ) + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.CommandCount", + "address": None, + "data": command_history.CommandCount, + } + ) + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.LastDisplayed", + "address": command_history.LastDisplayed.vol.offset, + "data": command_history.LastDisplayed, + } + ) + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.CommandCountMax", + "address": command_history.CommandCountMax.vol.offset, + "data": command_history.CommandCountMax, + } + ) + + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.CommandBucket", + "address": command_history.CommandBucket.vol.offset, + "data": "", + } + ) + for ( + cmd_index, + bucket_cmd, + ) in command_history.scan_command_bucket(): + try: + command_history_properties.append( + { + "name": f"_COMMAND_HISTORY.CommandBucket_Command_{cmd_index}", + "address": bucket_cmd.vol.offset, + "data": bucket_cmd.get_command(), + } + ) + except Exception as e: + vollog.debug( + f"reading {bucket_cmd} encountered exception {e}" + ) + except Exception as e: + vollog.debug( + f"reading {command_history} encountered exception {e}" + ) + + yield conhost_proc, command_history, command_history_properties + + def _generator( + self, procs: Generator[interfaces.objects.ObjectInterface, None, None] + ): + """ + Generates the command history to use in rendering + + Args: + procs: the process list filtered to conhost.exe instances + """ + + kernel = self.context.modules[self.config["kernel"]] + + max_history = set(self.config.get("max_history", [50])) + no_registry = self.config.get("no_registry") + + if no_registry is False: + max_history, _max_buffers = consoles.Consoles.get_console_settings_from_registry( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + max_history, + [], + ) + + vollog.debug(f"Possible CommandHistorySize values: {max_history}") + + for proc, command_history, command_history_properties in self.get_command_history( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + self.config_path, + procs, + max_history, + ): + process_name = utility.array_to_string(proc.ImageFileName) + + if command_history and command_history_properties: + for command_history_property in command_history_properties: + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(command_history.vol.offset), + command_history_property["name"], + ( + renderers.NotApplicableValue() + if command_history_property["address"] is None + else format_hints.Hex(command_history_property["address"]) + ), + str(command_history_property["data"]), + ), + ) + + def _conhost_proc_filter(self, proc): + """ + Used to filter to only conhost.exe processes + """ + process_name = utility.array_to_string(proc.ImageFileName) + + return process_name != "conhost.exe" + + def run(self): + kernel = self.context.modules[self.config["kernel"]] + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("ConsoleInfo", format_hints.Hex), + ("Property", str), + ("Address", format_hints.Hex), + ("Data", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=self._conhost_proc_filter, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index f6bbb3ae74..509b6ae9d6 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -363,7 +363,7 @@ def get_console_info( interfaces.context.ContextInterface, Dict[str, Any], ]: - """Extracts the cmdline from PEB + """Gets the Console Information structure and its related properties for each conhost process Args: context: The context to retrieve required elements (layers, symbol tables) from 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 3ae41fdd79..a4adf8028f 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 @@ -232,7 +232,7 @@ "kind": "base", "name": "unsigned int" }, - "offset": 20 + "offset": 24 } }, "kind": "struct", 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 b74862598f..5cc7ab6f58 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json @@ -232,7 +232,7 @@ "kind": "base", "name": "unsigned int" }, - "offset": 20 + "offset": 24 } }, "kind": "struct", diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 2db1f4cad9..279ee723e6 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -220,6 +220,12 @@ def get_original_title(self): class COMMAND(objects.StructType): """A Command Structure""" + def is_valid(self): + if self.Length < 1 or self.Allocated < 1 or self.Length > 1024 or self.Allocated > 1024: + return False + + return True + def get_command(self): if self.Length < 8: return self.Chars.cast( @@ -243,6 +249,26 @@ def CommandCount(self): command_size = self._context.symbol_space.get_type(command_type).size return int((self.CommandBucket.End - self.CommandBucket.Begin) / command_size) + @property + def ProcessHandle(self): + """ Allow ProcessHandle to be referenced regardless of OS version """ + return self.ConsoleProcessHandle.ProcessHandle + + def is_valid(self, max_history=50): + # The count must be between zero and max + if self.CommandCount < 0 or self.CommandCount > max_history: + return False + + # Last displayed must be between -1 and max + if self.LastDisplayed < -1 or self.LastDisplayed > max_history: + return False + + # Process handle must be a valid pid + if self.ProcessHandle <= 0 or self.ProcessHandle > 0xFFFF or self.ProcessHandle % 4 != 0: + return False + + return True + def get_application(self): if self.Application.Length < 8: return self.Application.Chars.cast( @@ -265,6 +291,7 @@ def scan_command_bucket(self, end=None): self.vol.type_name ).size command_size = self._context.symbol_space.get_type(command_type).size + if end is None: end = max( self.CommandBucket.EndCapacity, @@ -272,7 +299,9 @@ def scan_command_bucket(self, end=None): ) for i, pointer in enumerate(range(self.CommandBucket.Begin, end, command_size)): - yield i, self._context.object(command_type, self.vol.layer_name, pointer) + cmd = self._context.object(command_type, self.vol.layer_name, pointer) + if cmd.is_valid(): + yield i, cmd def get_commands(self): """Generator for commands in the history buffer. From 1dcaf9c0b15dfd00f2b35846488f19f3982ae5b2 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 13:50:51 +1000 Subject: [PATCH 017/250] 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 be31e298cb..599fedb6fb 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1839,7 +1839,7 @@ def get_inode_number(self) -> int: """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 @@ def get_access_time(self) -> datetime: 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 @@ def get_modification_time(self) -> datetime: 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 @@ def get_change_time(self) -> datetime: Returns: A datetime with the inode's last change time """ - return self.___time_member_to_datetime("i_ctime") + return self._time_member_to_datetime("i_ctime") def get_file_mode(self) -> str: """Returns the inode's file mode as string of the form '-rwxrwxrwx'. From 930d29046ad8c1cd47d6167e84118d78a3e549a6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:15:01 +1000 Subject: [PATCH 018/250] 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 599fedb6fb..1b5e1d286c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -7,10 +7,10 @@ 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 @@ 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: + def get_inode_type(self) -> Union[str, None]: """Returns inode type name Returns: @@ -1833,7 +1833,7 @@ def get_inode_type(self) -> str: 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 @@ 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( + return conversion.unixtime_to_datetime( self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9 ) elif self.has_member(f"__{member}"): From efba3a1b7336d5b31f7ac5ee1d8e99d95bcd74f6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 14:17:57 +1000 Subject: [PATCH 019/250] 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 1b5e1d286c..00f6730ebd 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1780,34 +1780,42 @@ def is_valid(self) -> bool: # 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 @@ def get_inode_type(self) -> Union[str, None]: Returns: The inode type name """ - if self.is_dir(): + if self.is_dir: return "DIR" - elif self.is_reg(): + elif self.is_reg: return "REG" - elif self.is_link(): + elif self.is_link: return "LNK" - elif self.is_fifo(): + elif self.is_fifo: return "FIFO" - elif self.is_sock(): + elif self.is_sock: return "SOCK" - elif self.is_char(): + elif self.is_char: return "CHR" - elif self.is_block(): + elif self.is_block: return "BLK" else: return None From d34b1ded3e673e2d311d14c8e104668d7ebac78c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Aug 2024 19:46:23 +1000 Subject: [PATCH 020/250] 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 00f6730ebd..05679523f3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1843,10 +1843,6 @@ def get_inode_type(self) -> Union[str, None]: else: return None - def get_inode_number(self) -> int: - """Returns the inode number""" - return int(self.i_ino) - def _time_member_to_datetime(self, member) -> datetime: if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"): # kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32 From 230ea09728dc9b756e27936544f03d43647cc0ba Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 17:52:51 +0200 Subject: [PATCH 021/250] Updating code following #1230 merge --- volatility3/framework/plugins/linux/lsof.py | 26 ++++++++--------- .../framework/symbols/linux/__init__.py | 28 ++++++++----------- .../symbols/linux/extensions/__init__.py | 14 ++++++++++ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index c1de48c1a1..fa9d2bf610 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -21,7 +21,6 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) @classmethod @@ -53,7 +52,7 @@ def list_fds_and_inodes( symbol_table: str, filter_func: Callable[[int], bool] = lambda _: False, ): - linuxutils_symbol_table = None # type: ignore + linuxutils_symbol_table = None for task in pslist.PsList.list_tasks(context, symbol_table, filter_func): if linuxutils_symbol_table is None: if constants.BANG not in task.vol.type_name: @@ -71,21 +70,17 @@ def list_fds_and_inodes( fd_num, filp, full_path = fd_fields inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) try: - inode_num, file_size, imode, ctime, mtime, atime = next( + inode_num, itype, file_size, imode, ctime, mtime, atime = next( inode_metadata ) except Exception as e: vollog.warning( f"Can't get inode metadata for file descriptor {fd_num}: {e}" ) - # Yield NotAvailableValue for each field in case of an exception - inode_num = renderers.NotAvailableValue() - file_size = renderers.NotAvailableValue() - imode = renderers.NotAvailableValue() - ctime = renderers.NotAvailableValue() - mtime = renderers.NotAvailableValue() - atime = renderers.NotAvailableValue() - yield pid, task_comm, task, fd_num, filp, full_path, inode_num, imode, ctime, mtime, atime, file_size + inode_num = itype = file_size = imode = ctime = mtime = atime = ( + renderers.NotAvailableValue() + ) + yield pid, task_comm, task, fd_num, filp, full_path, inode_num, itype, imode, ctime, mtime, atime, file_size def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) @@ -100,6 +95,7 @@ def _generator(self, pids, symbol_table): filp, full_path, inode_num, + itype, imode, ctime, mtime, @@ -112,6 +108,7 @@ def _generator(self, pids, symbol_table): fd_num, full_path, inode_num, + itype, imode, ctime, mtime, @@ -130,6 +127,7 @@ def run(self): ("FD", int), ("Path", str), ("Inode", int), + ("Type", str), ("Mode", str), ("Changed", datetime.datetime), ("Modified", datetime.datetime), @@ -144,6 +142,6 @@ def generate_timeline(self): for row in self._generator(pids, symbol_table): _depth, row_data = row description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' - yield description, timeliner.TimeLinerType.CHANGED, row_data[6] - yield description, timeliner.TimeLinerType.MODIFIED, row_data[7] - yield description, timeliner.TimeLinerType.ACCESSED, row_data[8] + yield description, timeliner.TimeLinerType.CHANGED, row_data[7] + yield description, timeliner.TimeLinerType.MODIFIED, row_data[8] + yield description, timeliner.TimeLinerType.ACCESSED, row_data[9] diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index a96fe9d2f1..d52c43dae3 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -280,23 +280,19 @@ def get_inode_metadata(cls, context: interfaces.context.ContextInterface, filp): A helper function that gets the inodes metadata from a file descriptor """ dentry = filp.get_dentry() - if dentry != 0: + if dentry: inode_object = dentry.d_inode - inode_num = inode_object.i_ino - file_size = inode_object.i_size # file size in bytes - imode = stat.filemode(inode_object.i_mode) # file type & Permissions - - # Timestamps - ctime = datetime.datetime.fromtimestamp( - inode_object.i_ctime.tv_sec - ) # last change time - mtime = datetime.datetime.fromtimestamp( - inode_object.i_mtime.tv_sec - ) # last modify time - atime = datetime.datetime.fromtimestamp( - inode_object.i_atime.tv_sec - ) # last access time - yield inode_num, file_size, imode, ctime, mtime, atime + if inode_object and inode_object.is_valid(): + itype = inode_object.get_inode_type() or "?" + yield ( + inode_object.i_ino, + itype, + inode_object.i_size, + inode_object.get_file_mode(), + inode_object.get_change_time(), + inode_object.get_modification_time(), + inode_object.get_access_time(), + ) @classmethod def mask_mods_list( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 05679523f3..0ee6e7d958 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1820,6 +1820,16 @@ def is_sticky(self) -> bool: """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 + @property + def is_whiteout(self) -> bool: + """Returns True if the inode is a whiteout""" + return (self.i_mode & 0o140000) == 0o140000 + + @property + def is_overlay(self) -> bool: + """Returns True if the inode is an overlay""" + return (self.i_mode & 0o40000) == 0o40000 + def get_inode_type(self) -> Union[str, None]: """Returns inode type name @@ -1840,6 +1850,10 @@ def get_inode_type(self) -> Union[str, None]: return "CHR" elif self.is_block: return "BLK" + elif self.is_whiteout: + return "WHT" + elif self.is_overlay: + return "OVL" else: return None From 60b1c49e49864ce7cb5ae3a3491b6a7e9e40eef3 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 18:10:11 +0200 Subject: [PATCH 022/250] removing test code --- .../framework/symbols/linux/extensions/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0ee6e7d958..06d2e2bf4f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1820,16 +1820,6 @@ def is_sticky(self) -> bool: """Returns True if the sticky bit is set""" return (self.i_mode & stat.S_ISVTX) != 0 - @property - def is_whiteout(self) -> bool: - """Returns True if the inode is a whiteout""" - return (self.i_mode & 0o140000) == 0o140000 - - @property - def is_overlay(self) -> bool: - """Returns True if the inode is an overlay""" - return (self.i_mode & 0o40000) == 0o40000 - def get_inode_type(self) -> Union[str, None]: """Returns inode type name From 2e9b5b62faec7d8e7fa67dd1e9243013af7cbcb4 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 2 Aug 2024 18:11:29 +0200 Subject: [PATCH 023/250] removing test code --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 06d2e2bf4f..05679523f3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1840,10 +1840,6 @@ def get_inode_type(self) -> Union[str, None]: return "CHR" elif self.is_block: return "BLK" - elif self.is_whiteout: - return "WHT" - elif self.is_overlay: - return "OVL" else: return None From d4ed07f95b883183fcd28871e7fc7858e649347a Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 20:51:32 +1000 Subject: [PATCH 024/250] PR review fixes: Add fixme to remember we should move wintime_to_datetime/unixtime_to_datetime out of renderers --- volatility3/framework/renderers/conversion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index 864794860b..c8ddc19fdd 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -11,6 +11,7 @@ from volatility3.framework import interfaces, renderers +# FIXME: Move wintime_to_datetime() and unixtime_to_datetime() out of renderers, possibly framework.objects.utility def wintime_to_datetime( wintime: int, ) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: From 0dfb9d8a0ff9080eef10b7505f7e1955e0f728e1 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 20:57:23 +1000 Subject: [PATCH 025/250] Linux mountinfo: Add a method to yield all filesystem superblocks --- .../framework/plugins/linux/mountinfo.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index da743bb601..319c92cca1 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface): _required_framework_version = (2, 2, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -146,7 +146,7 @@ def get_mountinfo( def _get_tasks_mountpoints( self, tasks: Iterable[interfaces.objects.ObjectInterface], - filtered_by_pids: bool, + filtered_by_pids: bool = False, ): seen_mountpoints = set() for task in tasks: @@ -184,8 +184,8 @@ def _generator( self, tasks: Iterable[interfaces.objects.ObjectInterface], mnt_ns_ids: List[int], - mount_format: bool, - filtered_by_pids: bool, + mount_format: bool = False, + filtered_by_pids: bool = False, ) -> Iterable[Tuple[int, Tuple]]: show_filter_warning = False for task, mnt, mnt_ns_id in self._get_tasks_mountpoints( @@ -247,6 +247,31 @@ def _generator( "Could not filter by mount namespace id. This field is not available in this kernel." ) + def get_superblocks(self): + """Yield file system superblocks based on the task's mounted filesystems. + + Yields: + super_block: Kernel's struct super_block object + """ + # No filter so that we get all the mount namespaces from all tasks + pid_filter = pslist.PsList.create_pid_filter() + tasks = pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=pid_filter + ) + + seen_sb_ptr = set() + for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): + path_root = linux.LinuxUtilities.get_path_mnt(task, mnt) + if not path_root: + continue + + sb_ptr = mnt.get_mnt_sb() + if not sb_ptr or sb_ptr in seen_sb_ptr: + continue + seen_sb_ptr.add(sb_ptr) + + yield sb_ptr.dereference(), path_root + def run(self): pids = self.config.get("pids") mount_ns_ids = self.config.get("mntns") From 231f682b2769d7a5a6de0fc96008d44c06f0825c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:01:59 +1000 Subject: [PATCH 026/250] Linux: Improve mount's object extension method docstrings --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 05679523f3..de5e432d34 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -47,7 +47,7 @@ def mod_mem_type(self): ).choices except exceptions.SymbolError: vollog.debug( - f"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" + "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" ) # set to empty dict to show that the enum was not found, and so shouldn't be searched for again self._mod_mem_type = {} @@ -936,7 +936,8 @@ class mount(objects.StructType): MNT_RELATIME: "relatime", } - def get_mnt_sb(self): + def get_mnt_sb(self) -> int: + """Returns a pointer to the super_block""" if self.has_member("mnt"): return self.mnt.mnt_sb elif self.has_member("mnt_sb"): @@ -1251,6 +1252,7 @@ def has_parent(self) -> bool: return self._get_real_mnt().has_parent() def get_mnt_sb(self): + """Returns a pointer to the super_block""" return self.mnt_sb def get_flags_access(self) -> str: From a369a9e23cea620a791129d687bd2bbe9c4d4442 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:05:23 +1000 Subject: [PATCH 027/250] Linux: dentry object extension: Add a method to walk dentries subdirectories --- .../symbols/linux/extensions/__init__.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index de5e432d34..3bfbe168a3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -820,6 +820,26 @@ def d_ancestor(self, ancestor_dentry): current_dentry = current_dentry.d_parent return None + def get_subdirs(self) -> interfaces.objects.ObjectInterface: + """Walks dentry subdirs + + Yields: + A dentry object + """ + if self.has_member("d_sib") and self.has_member("d_children"): + # kernels >= 6.8 + walk_member = "d_sib" + list_head_member = self.d_children.first + elif self.has_member("d_child") and self.has_member("d_subdirs"): + # 2.5.0 <= kernels < 6.8 + walk_member = "d_child" + list_head_member = self.d_subdirs + else: + raise exceptions.VolatilityException("Unsupported dentry type") + + dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry" + yield from list_head_member.to_list(dentry_type_name, walk_member) + class struct_file(objects.StructType): def get_dentry(self) -> interfaces.objects.ObjectInterface: From 41684478ad7c12c2d862ac8612f44315bc6f855b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:21:49 +1000 Subject: [PATCH 028/250] Linux: Add page cache support, including abstractions like RadixTree, XArray, and IDR, to support both older and latest kernel versions --- .../framework/symbols/linux/__init__.py | 346 +++++++++++++++++- .../symbols/linux/extensions/__init__.py | 244 +++++++++++- 2 files changed, 588 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 03353135d0..248cb8d751 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,6 +1,8 @@ # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import math +from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework @@ -30,6 +32,9 @@ def __init__(self, *args, **kwargs) -> None: self.set_type_class("kobject", extensions.kobject) self.set_type_class("cred", extensions.cred) self.set_type_class("inode", extensions.inode) + self.set_type_class("idr", extensions.IDR) + self.set_type_class("address_space", extensions.address_space) + self.set_type_class("page", extensions.page) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) @@ -67,7 +72,7 @@ def __init__(self, *args, **kwargs) -> None: class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 1, 0) + _version = (2, 2, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -425,3 +430,342 @@ def get_module_from_volobj_type( kernel = context.modules[kernel_module_name] return kernel + + @classmethod + def choose_kernel_tree(cls, vmlinux: interfaces.context.ModuleInterface) -> "Tree": + """Returns the appropriate tree data structure instance for the current kernel implementation. + This is used by the IDR and the PageCache to choose between the XArray and RadixTree. + + Args: + vmlinux: The kernel module object + + Returns: + The appropriate Tree instance for the current kernel + """ + address_space_type = vmlinux.get_type("address_space") + address_space_has_i_pages = address_space_type.has_member("i_pages") + i_pages_type_name = ( + address_space_type.child_template("i_pages").vol.type_name + if address_space_has_i_pages + else "" + ) + i_pages_is_xarray = i_pages_type_name.endswith(constants.BANG + "xarray") + i_pages_is_radix_tree_root = i_pages_type_name.endswith( + constants.BANG + "radix_tree_root" + ) and vmlinux.get_type("radix_tree_root").has_member("xa_head") + + if i_pages_is_xarray or i_pages_is_radix_tree_root: + return XArray(vmlinux) + else: + return RadixTree(vmlinux) + + +class Tree(ABC): + """Abstraction to support both XArray and RadixTree""" + + # Dynamic values, these will be initialized later + CHUNK_SHIFT = None + CHUNK_SIZE = None + CHUNK_MASK = None + + def __init__(self, vmlinux: interfaces.context.ModuleInterface): + self.vmlinux = vmlinux + self.vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + self.pointer_size = self.vmlinux.get_type("pointer").size + # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on + # the node.slots[] array size + node_type = self.vmlinux.get_type(self.node_type_name) + slots_array_size = node_type.child_template("slots").count + + # Calculate the LSB index - 1 + self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 + self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT + self.CHUNK_MASK = self.CHUNK_SIZE - 1 + + @property + @abstractmethod + def node_type_name(self) -> str: + """Returns the Tree implementation node type name + + Returns: + A string with the node type name + """ + raise NotImplementedError() + + @property + def tag_internal_value(self) -> int: + """Returns the internal node flag for the tree""" + raise NotImplementedError() + + @abstractmethod + def node_is_internal(self, nodep) -> bool: + """Checks if the node is internal""" + raise NotImplementedError + + @abstractmethod + def is_node_tagged(self, nodep) -> bool: + """Checks if the node pointer is tagged""" + raise NotImplementedError + + @abstractmethod + def untag_node(self, nodep) -> int: + """Untags a node pointer""" + raise NotImplementedError + + @abstractmethod + def get_tree_height(self, treep) -> int: + """Returns the tree height""" + raise NotImplementedError + + @abstractmethod + def get_node_height(self, nodep) -> int: + """Returns the node height""" + raise NotImplementedError + + @abstractmethod + def get_head_node(self, tree) -> int: + """Returns a pointer to the tree's head""" + raise NotImplementedError + + @abstractmethod + def is_valid_node(self, nodep) -> bool: + """Validates a node pointer""" + raise NotImplementedError + + def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface: + """Instanciates a tree node from its pointer + + Args: + nodep: Pointer to the XArray/RadixTree node + + Returns: + A XArray/RadixTree node instance + """ + node = self.vmlinux.object(self.node_type_name, offset=nodep, absolute=True) + return node + + def _slot_to_nodep(self, slot) -> int: + if self.node_is_internal(slot): + nodep = slot & ~self.tag_internal_value + else: + nodep = slot + + return nodep + + def _iter_node(self, nodep, height) -> int: + node = self.nodep_to_node(nodep) + node_slots = node.slots + for off in range(self.CHUNK_SIZE): + slot = node_slots[off] + if slot == 0: + continue + + nodep = self._slot_to_nodep(slot) + + if height == 1: + if self.is_valid_node(nodep): + yield nodep + else: + for child_node in self._iter_node(nodep, height - 1): + yield child_node + + def get_page_addresses(self, root: interfaces.objects.ObjectInterface) -> int: + """Walks the tree data structure + + Args: + root: The tree root object + + Yields: + A tree node pointer + """ + height = self.get_tree_height(root.vol.offset) + + nodep = self.get_head_node(root) + if not nodep: + return + + # Keep the internal flag before untagging it + is_internal = self.node_is_internal(nodep) + if self.is_node_tagged(nodep): + nodep = self.untag_node(nodep) + + if is_internal: + height = self.get_node_height(nodep) + + if height == 0: + if self.is_valid_node(nodep): + yield nodep + else: + for child_node in self._iter_node(nodep, height): + yield child_node + + +class XArray(Tree): + XARRAY_TAG_MASK = 3 + XARRAY_TAG_INTERNAL = 2 + + def get_tree_height(self, treep) -> int: + return 0 + + @property + def node_type_name(self) -> str: + return "xa_node" + + @property + def tag_internal_value(self) -> int: + return self.XARRAY_TAG_INTERNAL + + def get_node_height(self, nodep) -> int: + node = self.nodep_to_node(nodep) + return (node.shift / self.CHUNK_SHIFT) + 1 + + def get_head_node(self, tree) -> int: + return tree.xa_head + + def node_is_internal(self, nodep) -> bool: + return (nodep & self.XARRAY_TAG_MASK) == self.XARRAY_TAG_INTERNAL + + def is_node_tagged(self, nodep) -> bool: + return (nodep & self.XARRAY_TAG_MASK) != 0 + + def untag_node(self, nodep) -> int: + return nodep & (~self.XARRAY_TAG_MASK) + + def is_valid_node(self, nodep) -> bool: + # It should have the tag mask clear + return not self.is_node_tagged(nodep) + + +class RadixTree(Tree): + RADIX_TREE_INTERNAL_NODE = 1 + RADIX_TREE_EXCEPTIONAL_ENTRY = 2 + RADIX_TREE_ENTRY_MASK = 3 + + # Dynamic values. These will be initialized later + RADIX_TREE_INDEX_BITS = None + RADIX_TREE_MAX_PATH = None + RADIX_TREE_HEIGHT_SHIFT = None + RADIX_TREE_HEIGHT_MASK = None + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + char_bits = 8 + self.RADIX_TREE_INDEX_BITS = char_bits * self.pointer_size + self.RADIX_TREE_MAX_PATH = int( + math.ceil(self.RADIX_TREE_INDEX_BITS / float(self.CHUNK_SHIFT)) + ) + self.RADIX_TREE_HEIGHT_SHIFT = self.RADIX_TREE_MAX_PATH + 1 + self.RADIX_TREE_HEIGHT_MASK = (1 << self.RADIX_TREE_HEIGHT_SHIFT) - 1 + + if not self.vmlinux.has_type("radix_tree_root"): + # In kernels 4.20, RADIX_TREE_INTERNAL_NODE flag took RADIX_TREE_EXCEPTIONAL_ENTRY's + # value. RADIX_TREE_EXCEPTIONAL_ENTRY was removed but that's managed in is_valid_node() + # Note that the Radix Tree is still in use for IDR, even after kernels 4.20 when XArray + # mostly replace it + self.RADIX_TREE_INTERNAL_NODE = 2 + + @property + def node_type_name(self) -> str: + return "radix_tree_node" + + @property + def tag_internal_value(self) -> int: + return self.RADIX_TREE_INTERNAL_NODE + + def get_tree_height(self, treep) -> int: + try: + if self.vmlinux.get_type("radix_tree_root").has_member("height"): + # kernels < 4.7.10 + radix_tree_root = self.vmlinux.object( + "radix_tree_root", offset=treep, absolute=True + ) + return radix_tree_root.height + except exceptions.SymbolError: + pass + + # kernels >= 4.7.10 + return 0 + + def _radix_tree_maxindex(self, node, height) -> int: + """Return the maximum key which can be store into a radix tree with this height.""" + + if not self.vmlinux.has_symbol("height_to_maxindex"): + # Kernels >= 4.7 + return (self.CHUNK_SIZE << node.shift) - 1 + else: + # Kernels < 4.7 + height_to_maxindex_array = self.vmlinux.object_from_symbol( + "height_to_maxindex" + ) + maxindex = height_to_maxindex_array[height] + return maxindex + + def get_node_height(self, nodep) -> int: + node = self.nodep_to_node(nodep) + if hasattr(node, "shift"): + # 4.7 <= Kernels < 4.20 + return (node.shift / self.CHUNK_SHIFT) + 1 + elif hasattr(node, "path"): + # 3.15 <= Kernels < 4.7 + return node.path & self.RADIX_TREE_HEIGHT_MASK + elif hasattr(node, "height"): + # Kernels < 3.15 + return node.height + else: + raise exceptions.VolatilityException("Cannot find radix-tree node height") + + def get_head_node(self, tree) -> int: + return tree.rnode + + def node_is_internal(self, nodep) -> bool: + return (nodep & self.RADIX_TREE_INTERNAL_NODE) != 0 + + def is_node_tagged(self, nodep) -> bool: + return self.node_is_internal(nodep) + + def untag_node(self, nodep) -> int: + return nodep & (~self.RADIX_TREE_ENTRY_MASK) + + def is_valid_node(self, nodep) -> bool: + # In kernels 4.20, exceptional nodes were removed and internal entries took their bitmask + if self.vmlinux.has_type("radix_tree_root"): + return ( + nodep & self.RADIX_TREE_ENTRY_MASK + ) != self.RADIX_TREE_EXCEPTIONAL_ENTRY + + return True + + +class PageCache(object): + """Linux Page Cache abstraction""" + + def __init__( + self, + page_cache: interfaces.objects.ObjectInterface, + vmlinux: interfaces.context.ModuleInterface, + ): + """ + Args: + page_cache: Page cache address space + vmlinux: Kernel module object + """ + self.vmlinux = vmlinux + self._page_cache = page_cache + self._tree = LinuxUtilities.choose_kernel_tree(self.vmlinux) + + def get_cached_pages(self) -> interfaces.objects.ObjectInterface: + """Returns all page cache contents + + Yields: + Page objects + """ + + for page_addr in self._tree.get_page_addresses(self._page_cache.i_pages): + if not page_addr: + continue + + page = self.vmlinux.object("page", offset=page_addr, absolute=True) + if page: + yield page diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3bfbe168a3..300ab2ed0c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,10 +4,11 @@ import collections.abc import logging +import functools import stat from datetime import datetime import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union +from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion @@ -1919,3 +1920,244 @@ def get_file_mode(self) -> str: The inode's file mode string """ return stat.filemode(self.i_mode) + + def get_pages(self) -> interfaces.objects.ObjectInterface: + """Gets the inode's cached pages + + Yields: + The inode's cached pages + """ + if not self.i_size: + return + elif not (self.i_mapping and self.i_mapping.nrpages > 0): + return + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + page_cache = linux.PageCache(self.i_mapping.dereference(), vmlinux) + yield from page_cache.get_cached_pages() + + def get_contents(self): + """Get the inode cached pages from the page cache + + Yields: + page_index (int): The page index in the Tree. File offset is page_index * PAGE_SIZE. + page_content (str): The page content + """ + for page_obj in self.get_pages(): + page_index = int(page_obj.index) + page_content = page_obj.get_content() + yield page_index, page_content + + +class address_space(objects.StructType): + @property + def i_pages(self): + """Returns the appropriate member containing the page cache tree""" + if self.has_member("i_pages"): + # Kernel >= 4.17 + return self.member("i_pages") + elif self.has_member("page_tree"): + # Kernel < 4.17 + return self.member("page_tree") + + raise exceptions.VolatilityException("Unsupported page cache tree") + + +class page(objects.StructType): + @property + @functools.cache + def pageflags_enum(self) -> Dict: + """Returns 'pageflags' enumeration key/values + + Returns: + A dictionary with the pageflags enumeration key/values + """ + # FIXME: It would be even better to use @functools.cached_property instead, + # however, this requires Python +3.8 + try: + pageflags_enum = self._context.symbol_space.get_enumeration( + self.get_symbol_table_name() + constants.BANG + "pageflags" + ).choices + except exceptions.SymbolError: + vollog.debug( + "Unable to find pageflags enum. This can happen in kernels < 2.6.26 or wrong ISF" + ) + # set to empty dict to show that the enum was not found, and so shouldn't be searched for again + pageflags_enum = {} + + return pageflags_enum + + def flags_list(self) -> List[str]: + """Returns a list of page flags + + Returns: + List of page flags + """ + flags = [] + for name, value in self.pageflags_enum.items(): + if self.flags & (1 << value) != 0: + flags.append(name) + + return flags + + def to_paddr(self) -> int: + """Converts a page's virtual address to its physical address using the current physical memory model. + + Returns: + int: page physical address + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + vmemmap_start = None + if vmlinux.has_symbol("mem_section"): + # SPARSEMEM_VMEMMAP physical memory model: memmap is virtually contiguous + if vmlinux.has_symbol("vmemmap_base"): + # CONFIG_DYNAMIC_MEMORY_LAYOUT - KASLR kernels >= 4.9 + vmemmap_start = vmlinux.object_from_symbol("vmemmap_base") + else: + # !CONFIG_DYNAMIC_MEMORY_LAYOUT + if vmlinux_layer._maxvirtaddr < 57: + # 4-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L4 + vmemmap_base_l4 = 0xFFFFEA0000000000 + vmemmap_start = vmemmap_base_l4 + else: + # 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5 + vmemmap_base_l5 = 0xFFD4000000000000 + vmemmap_start = vmemmap_base_l5 + + # FIXME: Remove this exception once 5-level paging is supported. + raise exceptions.VolatilityException( + "5-level paging is not yet supported" + ) + + elif vmlinux.has_symbol("mem_map"): + # FLATMEM physical memory model, typically 32bit + vmemmap_start = vmlinux.object_from_symbol("mem_map") + + elif vmlinux.has_symbol("node_data"): + raise exceptions.VolatilityException("NUMA systems are not yet supported") + else: + raise exceptions.VolatilityException("Unsupported Linux memory model") + + if not vmemmap_start: + raise exceptions.VolatilityException( + "Something went wrong, we shouldn't be here" + ) + + page_type_size = vmlinux.get_type("page").size + pagec = vmlinux_layer.canonicalize(self.vol.offset) + pfn = (pagec - vmemmap_start) // page_type_size + page_paddr = pfn * vmlinux_layer.page_size + + return page_paddr + + def get_content(self) -> Union[str, None]: + """Returns the page content + + Returns: + The page content + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + physical_layer = vmlinux.context.layers["memory_layer"] + page_paddr = self.to_paddr() + if not page_paddr: + return + + page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size) + return page_data + + +class IDR(objects.StructType): + IDR_BITS = 8 + IDR_MASK = (1 << IDR_BITS) - 1 + INT_SIZE = 4 + MAX_IDR_SHIFT = INT_SIZE * 8 - 1 + MAX_IDR_BIT = 1 << MAX_IDR_SHIFT + + def idr_max(self, num_layers: int) -> int: + """Returns the maximum ID which can be allocated given idr::layers + + Args: + num_layers: Number of layers + + Returns: + Maximum ID for a given number of layers + """ + # Kernel < 4.17 + bits = min([self.INT_SIZE, num_layers * self.IDR_BITS, self.MAX_IDR_SHIFT]) + + return (1 << bits) - 1 + + def idr_find(self, idr_id: int) -> int: + """Finds an ID within the IDR data structure. + Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11 + Args: + idr_id: The IDR element ID + + Returns: + A pointer to the given ID element + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + if not vmlinux.get_type("idr_layer").has_member("layer"): + vollog.info( + "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" + ) + return + + if idr_id < 0: + return + + cur_layer = self.top + if not cur_layer: + return + + n = (cur_layer.layer + 1) * self.IDR_BITS + + if idr_id > self.idr_max(cur_layer.layer + 1): + return + + assert n != 0 + + while n > 0 and cur_layer: + n -= self.IDR_BITS + assert n == cur_layer.layer * self.IDR_BITS + cur_layer = cur_layer.ary[(idr_id >> n) & self.IDR_MASK] + + return cur_layer.v() + + def _old_kernel_get_page_addresses(self, in_use) -> int: + # Kernels < 4.11 + total = next_id = 0 + while total < in_use: + page_addr = self.idr_find(next_id) + if page_addr: + yield page_addr + total += 1 + + next_id += 1 + + def _new_kernel_get_page_addresses(self, _in_use) -> int: + # Kernels >= 4.11 + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) + for page_addr in tree.get_page_addresses(root=self.idr_rt): + yield page_addr + + def get_page_addresses(self, in_use=0) -> int: + """Walks the IDR and yield a pointer associated with each element. + + Args: + in_use (int, optional): _description_. Defaults to 0. + + Yields: + A pointer associated with each element. + """ + if self.has_member("idr_rt"): + get_page_addresses_func = self._new_kernel_get_page_addresses + else: + get_page_addresses_func = self._old_kernel_get_page_addresses + + for page_addr in get_page_addresses_func(in_use): + yield page_addr From ac27d6663a3733e002cb064afd155139103c4c43 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:26:29 +1000 Subject: [PATCH 029/250] Linux: Add two page cache plugins, linux.pagecache.Files and linux.pagecache.InodePages --- .../framework/plugins/linux/pagecache.py | 504 ++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 volatility3/framework/plugins/linux/pagecache.py diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py new file mode 100644 index 0000000000..545c243e0a --- /dev/null +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -0,0 +1,504 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import math +import logging +import datetime +from dataclasses import dataclass, astuple +from typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.plugins import timeliner +from volatility3.plugins.linux import mountinfo + +vollog = logging.getLogger(__name__) + + +@dataclass +class InodeUser: + """Inode user representation, featuring augmented information and formatted fields. + This is the data the plugin will eventually display. + """ + + superblock_addr: int + mountpoint: str + device: str + inode_num: int + inode_addr: int + type: str + inode_pages: int + cached_pages: int + file_mode: str + access_time: str + modification_time: str + change_time: str + path: str + + +@dataclass +class InodeInternal: + """Inode internal representation containing only the core objects + + Fields: + superblock: 'super_block' struct + mountpoint: Superblock mountpoint path + inode: 'inode' struct + path: Dentry full path + """ + + superblock: interfaces.objects.ObjectInterface + mountpoint: str + inode: interfaces.objects.ObjectInterface + path: str + + def to_user( + self, kernel_layer: interfaces.layers.TranslationLayerInterface + ) -> InodeUser: + """Augment the inode information to be presented to the user + + Args: + kernel_layer: The kernel layer to obtain the page size + + Returns: + An InodeUser dataclass + """ + # Ensure all types are atomic immutable. Otherwise, astuple() will take a long + # time doing a deepcopy of the Volatility objects. + superblock_addr = self.superblock.vol.offset + device = f"{self.superblock.major}:{self.superblock.minor}" + inode_num = int(self.inode.i_ino) + inode_addr = self.inode.vol.offset + inode_type = renderers.UnparsableValue() + # Round up the number of pages to fit the inode's size + inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size))) + cached_pages = int(self.inode.i_mapping.nrpages) + file_mode = self.inode.get_file_mode() + access_time_dt = self.inode.get_access_time() + modification_time_str = self.inode.get_modification_time() + change_time_str = self.inode.get_change_time() + + inode_user = InodeUser( + superblock_addr=superblock_addr, + mountpoint=self.mountpoint, + device=device, + inode_num=inode_num, + inode_addr=inode_addr, + type=inode_type, + inode_pages=inode_pages, + cached_pages=cached_pages, + file_mode=file_mode, + access_time=access_time_dt, + modification_time=modification_time_str, + change_time=change_time_str, + path=self.path, + ) + return inode_user + + +class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): + """Lists files from memory""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 1, 0) + ), + requirements.ListRequirement( + name="type", + description="List of space-separated file type filters i.e. --type REG DIR", + element_type=str, + optional=True, + ), + requirements.StringRequirement( + name="find", + description="Filename (full path) to find", + optional=True, + ), + ] + + @staticmethod + def _follow_symlink(inode, symlink_path) -> str: + """Follows (fast) symlinks (kernels >= 4.2.x). + Fast symlinks are filesystem agnostic. + + Args: + inode: The inode (or pointer) to dump + symlink_path: The symlink name + + Returns: + If it can resolve the symlink, it returns a string "symlink_path -> target_path" + Otherwise, it returns the same symlink_path + """ + # i_link (fast symlinks) were introduced in 4.2 + if inode and inode.is_link and inode.has_member("i_link") and inode.i_link: + i_link_str = inode.i_link.dereference().cast( + "string", max_length=255, encoding="utf-8", errors="replace" + ) + symlink_path = f"{symlink_path} -> {i_link_str}" + + return symlink_path + + @classmethod + def _walk_dentry(cls, seen_dentries, root_dentry, parent): + + for dentry in root_dentry.get_subdirs(): + dentry_addr = dentry.vol.offset + + # corruption + if dentry_addr == root_dentry.vol.offset: + continue + + if dentry_addr in seen_dentries: + continue + + seen_dentries.add(dentry_addr) + + inode = dentry.d_inode + if not (inode and inode.is_valid()): + continue + + # This allows us to have consistent paths + if dentry.d_name.name: + name = dentry.d_name.name_as_str() + # Do NOT use os.path.join() below + new_file = parent + "/" + name + else: + continue + + yield new_file, dentry, dentry.d_parent.vol.offset + + if inode.is_dir: + for new_file, dentry, parent_address in cls._walk_dentry( + seen_dentries, dentry, new_file + ): + yield new_file, dentry, parent_address + + @classmethod + def get_inodes( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + ): + """Retrieves the inodes from the superblocks + + Args: + context: The context that the plugin will operate within + config_path: The path to configuration data within the context configuration data + + Yields: + An InodeInternal object + """ + + superblocks_iter = mountinfo.MountInfo( + context=context, + config_path=config_path, + ).get_superblocks() + + seen_inodes = set() + seen_dentries = set() + for superblock, mountpoint in superblocks_iter: + parent = "" if mountpoint == "/" else mountpoint + + # Superblock root dentry + root_dentry = superblock.s_root + if not root_dentry: + continue + + # Dentry sanity check + if not root_dentry.is_root(): + continue + + # More dentry/inode sanity checks + root_inode_ptr = root_dentry.d_inode + if not root_inode_ptr: + continue + root_inode = root_inode_ptr.dereference() + if not root_inode.is_valid(): + continue + + # Inode already processed? + if root_inode_ptr in seen_inodes: + continue + seen_inodes.add(root_inode_ptr) + + root_path = mountpoint + + inode_in = InodeInternal( + superblock=superblock, + mountpoint=mountpoint, + inode=root_inode, + path=root_path, + ) + yield inode_in + + # Children + for file_path, file_dentry, _ in cls._walk_dentry( + seen_dentries, root_dentry, parent + ): + if not file_dentry: + continue + # Dentry/inode sanity checks + file_inode_ptr = file_dentry.d_inode + if not file_inode_ptr: + continue + file_inode = file_inode_ptr.dereference() + if not file_inode.is_valid(): + continue + + # Inode already processed? + if file_inode_ptr in seen_inodes: + continue + seen_inodes.add(file_inode_ptr) + + file_path = cls._follow_symlink(file_inode_ptr, file_path) + inode_in = InodeInternal( + superblock=superblock, + mountpoint=mountpoint, + inode=file_inode, + path=file_path, + ) + yield inode_in + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + inodes_iter = self.get_inodes( + context=self.context, config_path=self.config_path + ) + + types_filter = self.config["type"] + for inode_in in inodes_iter: + if types_filter and inode_in.inode.get_inode_type() not in types_filter: + continue + + if self.config["find"]: + if inode_in.path == self.config["find"]: + inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) + break # Only the first match + else: + inode_out = inode_in.to_user(vmlinux_layer) + yield (0, astuple(inode_out)) + + def generate_timeline(self): + """Generates tuples of (description, timestamp_type, timestamp) + + These need not be generated in any particular order, sorting + will be done later + """ + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + inodes_iter = self.get_inodes( + context=self.context, config_path=self.config_path + ) + for inode_in in inodes_iter: + inode_out = inode_in.to_user(vmlinux_layer) + description = f"Cached Inode for {inode_out.path}" + yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time + yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time + yield description, timeliner.TimeLinerType.CHANGE, inode_out.change_time + + @staticmethod + def format_fields_with_headers(headers, generator): + """Uses the headers type to cast the fields obtained from the generator""" + for level, fields in generator: + formatted_fields = [] + for header, field in zip(headers, fields): + header_type = header[1] + + if isinstance( + field, (header_type, interfaces.renderers.BaseAbsentValue) + ): + formatted_field = field + else: + formatted_field = header_type(field) + + formatted_fields.append(formatted_field) + yield level, formatted_fields + + def run(self): + headers = [ + ("SuperblockAddr", format_hints.Hex), + ("MountPoint", str), + ("Device", str), + ("InodeNum", int), + ("InodeAddr", format_hints.Hex), + ("FileType", str), + ("InodePages", int), + ("CachedPages", int), + ("FileMode", str), + ("AccessTime", datetime.datetime), + ("ModificationTime", datetime.datetime), + ("ChangeTime", datetime.datetime), + ("FilePath", str), + ] + + return renderers.TreeGrid( + headers, self.format_fields_with_headers(headers, self._generator()) + ) + + +class InodePages(plugins.PluginInterface): + """Lists and recovers cached inode pages""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="files", plugin=Files, version=(1, 0, 0) + ), + requirements.StringRequirement( + name="find", + description="Filename (full path) to find ", + optional=True, + ), + requirements.IntRequirement( + name="inode", + description="Inode address", + optional=True, + ), + requirements.StringRequirement( + name="dump", + description="Output file path", + optional=True, + ), + ] + + @staticmethod + def write_inode_content_to_file( + inode: interfaces.objects.ObjectInterface, + filename: str, + vmlinux_layer: interfaces.layers.TranslationLayerInterface, + ) -> None: + """Extracts the inode's contents from the page cache and saves them to a file + + Args: + inode: The inode to dump + filename: Filename for writing the inode content + vmlinux_layer: The kernel layer to obtain the page size + """ + if not inode.is_reg: + vollog.error("The inode is not a regular file") + return + + # By using truncate/seek, provided the filesystem supports it, a sparse file will be + # created, saving both disk space and I/O time. + # Additionally, using the page index will guarantee that each page is written at the + # appropriate file position. + try: + with open(filename, "wb") as f: + inode_size = inode.i_size + f.truncate(inode_size) + + for page_idx, page_content in inode.get_contents(): + current_fp = page_idx * vmlinux_layer.page_size + max_length = inode_size - current_fp + page_bytes = page_content[:max_length] + if current_fp + len(page_bytes) > inode_size: + vollog.error( + "Page out of file bounds: inode 0x%x, inode size %d, page index %d", + inode.vol.object, + inode_size, + page_idx, + ) + f.seek(current_fp) + f.write(page_bytes) + + except IOError as e: + vollog.error("Unable to write to file (%s): %s", filename, e) + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + if self.config["inode"] and self.config["find"]: + vollog.error("Cannot use --inode and --find simultaneously") + return + + if self.config["find"]: + inodes_iter = Files.get_inodes( + context=self.context, config_path=self.config_path + ) + for inode_in in inodes_iter: + if inode_in.path == self.config["find"]: + inode = inode_in.inode + break # Only the first match + + elif self.config["inode"]: + inode = vmlinux.object("inode", self.config["inode"], absolute=True) + else: + vollog.error("You must use either --inode or --find") + return + + if not inode.is_reg: + vollog.error("The inode is not a regular file") + return + + inode_size = inode.i_size + if not inode.is_valid(): + vollog.error("Invalid inode at 0x%x", self.config["inode"]) + return + + for page_obj in inode.get_pages(): + page_vaddr = page_obj.vol.offset + page_paddr = page_obj.to_paddr() + page_mapping_addr = page_obj.mapping + page_index = int(page_obj.index) + page_file_offset = page_index * vmlinux_layer.page_size + dump_safe = page_file_offset < inode_size + page_flags_list = page_obj.get_flags() + page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) + fields = ( + page_vaddr, + page_paddr, + page_mapping_addr, + page_index, + dump_safe, + page_flags, + ) + + yield 0, fields + + if self.config["dump"]: + filename = self.config["dump"] + vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename) + self.write_inode_content_to_file(inode, filename, vmlinux_layer) + + def run(self): + headers = [ + ("PageVAddr", format_hints.Hex), + ("PagePAddr", format_hints.Hex), + ("MappingAddr", format_hints.Hex), + ("Index", int), + ("DumpSafe", bool), + ("Flags", str), + ] + + return renderers.TreeGrid( + headers, Files.format_fields_with_headers(headers, self._generator()) + ) From 55212008f805abdd47f2f3d7d6211c198097f8d4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:39:51 +1000 Subject: [PATCH 030/250] Linux: Add pidhashtable plugin. This is based on the vol2 plugin, removing ancient kernel support, curating code and enhancing comments, while using the new IDR abstraction included also in this effort. --- .../framework/plugins/linux/pidhashtable.py | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 volatility3/framework/plugins/linux/pidhashtable.py diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py new file mode 100644 index 0000000000..b24c73e777 --- /dev/null +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -0,0 +1,249 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List + +from volatility3.framework import renderers, interfaces, constants +from volatility3.framework.symbols import linux +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class PIDHashTable(plugins.PluginInterface): + """Enumerates processes through the PID hash table""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0) + ), + requirements.BooleanRequirement( + name="decorate_comm", + description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", + optional=True, + default=False, + ), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.vmlinux = None + self.vmlinux_layer = None + + def _is_valid_task(self, task): + return task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent) + + def _get_pidtype_pid(self): + # The pid_type enumeration is present since 2.5.37, just in case + pid_type_enum = self.vmlinux.get_enumeration("pid_type") + if not pid_type_enum: + vollog.error("Cannot find pid_type enum. Unsupported kernel") + return + + pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID") + if pidtype_pid is None: + vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel") + return + + # Typically PIDTYPE_PID = 0 + return pidtype_pid + + def _get_pidhash_array(self): + pidhash_shift = self.vmlinux.object_from_symbol("pidhash_shift") + pidhash_size = 1 << pidhash_shift + + array_type_name = self.vmlinux.symbol_table_name + constants.BANG + "array" + + pidhash_ptr = self.vmlinux.object_from_symbol("pid_hash") + # pidhash is an array of hlist_heads + pidhash = self._context.object( + array_type_name, + offset=pidhash_ptr, + subtype=self.vmlinux.get_type("hlist_head"), + count=pidhash_size, + layer_name=self.vmlinux.layer_name, + ) + + return pidhash + + def _walk_upid(self, seen_upids, upid): + while upid and self.vmlinux_layer.is_valid(upid.vol.offset): + if upid.vol.offset in seen_upids: + break + seen_upids.add(upid.vol.offset) + + pid_chain = upid.pid_chain + if not (pid_chain and self.vmlinux_layer.is_valid(pid_chain.vol.offset)): + break + + upid = linux.LinuxUtilities.container_of( + pid_chain.next, "upid", "pid_chain", self.vmlinux + ) + + def _get_upids(self): + # 2.6.24 <= kernels < 4.15 + pidhash = self._get_pidhash_array() + + seen_upids = set() + for hlist in pidhash: + # each entry in the hlist is a upid which is wrapped in a pid + ent = hlist.first + + while ent and self.vmlinux_layer.is_valid(ent.vol.offset): + # upid->pid_chain exists 2.6.24 <= kernel < 4.15 + upid = linux.LinuxUtilities.container_of( + ent.vol.offset, "upid", "pid_chain", self.vmlinux + ) + + if upid.vol.offset in seen_upids: + break + + self._walk_upid(seen_upids, upid) + + ent = ent.next + + return seen_upids + + def _pid_hash_implementation(self): + # 2.6.24 <= kernels < 4.15 + task_pids_off = self.vmlinux.get_type("task_struct").relative_child_offset( + "pids" + ) + pidtype_pid = self._get_pidtype_pid() + + for upid in self._get_upids(): + pid = linux.LinuxUtilities.container_of( + upid, "pid", "numbers", self.vmlinux + ) + if not pid: + continue + + pid_tasks_0 = pid.tasks[pidtype_pid].first + if not pid_tasks_0: + continue + + task = self.vmlinux.object( + "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True + ) + if self._is_valid_task(task): + yield task + + def _task_for_radix_pid_node(self, nodep): + # kernels >= 4.15 + pid = self.vmlinux.object("pid", offset=nodep, absolute=True) + pidtype_pid = self._get_pidtype_pid() + + pid_tasks_0 = pid.tasks[pidtype_pid].first + if not pid_tasks_0: + return + + task_struct_type = self.vmlinux.get_type("task_struct") + if task_struct_type.has_member("pids"): + member = "pids" + elif task_struct_type.has_member("pid_links"): + member = "pid_links" + else: + return None + + task_pids_off = task_struct_type.relative_child_offset(member) + task = self.vmlinux.object( + "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True + ) + return task + + def _pid_namespace_idr(self): + # kernels >= 4.15 + ns_addr = self.vmlinux.get_symbol("init_pid_ns").address + ns = self.vmlinux.object("pid_namespace", offset=ns_addr) + + for page_addr in ns.idr.get_page_addresses(): + task = self._task_for_radix_pid_node(page_addr) + if self._is_valid_task(task): + yield task + + def _determine_pid_func(self): + pid_hash = self.vmlinux.has_symbol("pid_hash") and self.vmlinux.has_symbol( + "pidhash_shift" + ) # 2.5.55 <= kernels < 4.15 + + has_pid_numbers = self.vmlinux.has_type("pid") and self.vmlinux.get_type( + "pid" + ).has_member( + "numbers" + ) # kernels >= 2.6.24 + + has_pid_numbers = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + "upid" + ).has_member( + "pid_chain" + ) # 2.6.24 <= kernels < 4.15 + + # kernels >= 4.15 + pid_idr = self.vmlinux.has_type("pid_namespace") and self.vmlinux.get_type( + "pid_namespace" + ).has_member("idr") + + if pid_idr: + # kernels >= 4.15 + return self._pid_namespace_idr + elif pid_hash and has_pid_numbers and has_pid_numbers: + # 2.6.24 <= kernels < 4.15 + return self._pid_hash_implementation + + return None + + def get_tasks(self) -> interfaces.objects.ObjectInterface: + """Enumerates processes through the PID hash table + + Yields: + task_struct objects + """ + self.vmlinux = self.context.modules[self.config["kernel"]] + self.vmlinux_layer = self.context.layers[self.vmlinux.layer_name] + pid_func = self._determine_pid_func() + if not pid_func: + vollog.error("Cannot determine which PID hash table this kernel is using") + return + + yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid)) + + def _generator( + self, decorate_comm: bool = False + ) -> interfaces.objects.ObjectInterface: + for task in self.get_tasks(): + offset, pid, tid, ppid, name = pslist.PsList.get_task_fields( + task, decorate_comm + ) + fields = format_hints.Hex(offset), pid, tid, ppid, name + yield 0, fields + + def run(self): + decorate_comm = self.config.get("decorate_comm") + + headers = [ + ("OFFSET", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ] + return renderers.TreeGrid(headers, self._generator(decorate_comm=decorate_comm)) From 103537801ee0b49ca3475be11e1fe62670938e91 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 21:44:38 +1000 Subject: [PATCH 031/250] Linux: Add a basic eBPF program enumeration plugin to test and demonstrate using the IDR abstraction --- volatility3/framework/plugins/linux/ebpf.py | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 volatility3/framework/plugins/linux/ebpf.py diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py new file mode 100644 index 0000000000..33ba71fafc --- /dev/null +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -0,0 +1,78 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import binascii +import logging +from typing import List + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.interfaces import plugins +from volatility3.framework.configuration import requirements + +vollog = logging.getLogger(__name__) + + +class EBPF(plugins.PluginInterface): + """Enumerate eBPF programs""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: + """Enumerate eBPF programs walking its IDR. + + Args: + vmlinux: The kernel symbols object + + Yields: + eBPF program objects + """ + if not vmlinux.has_symbol("prog_idr"): + raise exceptions.VolatilityException( + "Cannot find the eBPF prog idr. Unsupported kernel" + ) + + prog_idr_addr = vmlinux.get_symbol("prog_idr").address + prog_idr = vmlinux.object("idr", offset=prog_idr_addr) + for page_addr in prog_idr.get_page_addresses(): + bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) + yield bpf_prog + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + bpf_prog_types = vmlinux.get_enumeration("bpf_prog_type") + for prog in self.get_ebpf_programs(vmlinux): + prog_addr = prog.vol.offset + prog_type = bpf_prog_types.lookup(prog.type) + prog_tag_addr = prog.tag.vol.offset + prog_tag_size = prog.tag.count + prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) + prog_tag = binascii.hexlify(prog_tag_bytes).decode() + prog_name = ( + utility.array_to_string(prog.aux.name) or renderers.NotAvailableValue() + ) + fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type) + yield (0, fields) + + def run(self): + headers = [ + ("Address", format_hints.Hex), + ("Name", str), + ("Tag", str), + ("Type", str), + ] + return renderers.TreeGrid(headers, self._generator()) From cc04f665e35989fea4c108de2bdd1b31016145ed Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 3 Aug 2024 23:47:28 +1000 Subject: [PATCH 032/250] Fix inode type --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 545c243e0a..c36f8a339d 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -72,7 +72,7 @@ def to_user( device = f"{self.superblock.major}:{self.superblock.minor}" inode_num = int(self.inode.i_ino) inode_addr = self.inode.vol.offset - inode_type = renderers.UnparsableValue() + inode_type = self.inode.get_inode_type() or renderers.UnparsableValue() # Round up the number of pages to fit the inode's size inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size))) cached_pages = int(self.inode.i_mapping.nrpages) From a515e571dcb9eef2fc268b707cc175951f0831b8 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sat, 3 Aug 2024 12:03:20 -0700 Subject: [PATCH 033/250] add Win10x64 17763 --- .../framework/plugins/windows/consoles.py | 8 + .../consoles/consoles-win10-17763-x64.json | 595 ++++++++++++++++++ .../symbols/windows/extensions/consoles.py | 5 + 3 files changed, 608 insertions(+) create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 509b6ae9d6..0ef9e990a7 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -203,6 +203,7 @@ def determine_conhost_version( version_dict = {} else: version_dict = { + (10, 0, 17763, 0): "consoles-win10-17763-x64", (10, 0, 20348, 1): "consoles-win10-20348-x64", (10, 0, 20348, 1970): "consoles-win10-20348-1970-x64", (10, 0, 20348, 2461): "consoles-win10-20348-2461-x64", @@ -580,6 +581,13 @@ def get_console_info( "data": command_history.CommandCount, } ) + console_properties.append( + { + "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_LastDisplayed", + "address": command_history.LastDisplayed.vol.offset, + "data": command_history.LastDisplayed, + } + ) for ( cmd_index, bucket_cmd, diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json new file mode 100644 index 0000000000..f11fb39b19 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json @@ -0,0 +1,595 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 34 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 144 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 148 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1736 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1672 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1800 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1384 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1360 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1368 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -760 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -752 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1392 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 32 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 36 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 48 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 58 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 112 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 279ee723e6..74ea80e847 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -20,8 +20,13 @@ def _valid_dbcs(self, c): 0x28, 0x30, 0x48, + 0x50, 0x60, 0x80, + 0xa8, + 0xc0, + 0xc8, + 0x98, 0xF8, 0xF0, 0xA0, From 583cffe960a65a136d80bab194765368220bd9bf Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sat, 3 Aug 2024 14:24:17 -0700 Subject: [PATCH 034/250] #816 - next console properties for better readability in treegrid --- .../framework/plugins/windows/cmdscan.py | 17 ++++++- .../framework/plugins/windows/consoles.py | 44 +++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 86a34a4f8f..1e435c56fd 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -175,6 +175,15 @@ def get_command_history( ) command_history_properties.append( { + "level": 0, + "name": "_COMMAND_HISTORY", + "address": command_history.vol.offset, + "data": "", + } + ) + command_history_properties.append( + { + "level": 1, "name": f"_COMMAND_HISTORY.Application", "address": command_history.Application.vol.offset, "data": command_history.get_application(), @@ -182,6 +191,7 @@ def get_command_history( ) command_history_properties.append( { + "level": 1, "name": f"_COMMAND_HISTORY.ProcessHandle", "address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset, "data": hex( @@ -191,6 +201,7 @@ def get_command_history( ) command_history_properties.append( { + "level": 1, "name": f"_COMMAND_HISTORY.CommandCount", "address": None, "data": command_history.CommandCount, @@ -198,6 +209,7 @@ def get_command_history( ) command_history_properties.append( { + "level": 1, "name": f"_COMMAND_HISTORY.LastDisplayed", "address": command_history.LastDisplayed.vol.offset, "data": command_history.LastDisplayed, @@ -205,6 +217,7 @@ def get_command_history( ) command_history_properties.append( { + "level": 1, "name": f"_COMMAND_HISTORY.CommandCountMax", "address": command_history.CommandCountMax.vol.offset, "data": command_history.CommandCountMax, @@ -213,6 +226,7 @@ def get_command_history( command_history_properties.append( { + "level": 1, "name": f"_COMMAND_HISTORY.CommandBucket", "address": command_history.CommandBucket.vol.offset, "data": "", @@ -225,6 +239,7 @@ def get_command_history( try: command_history_properties.append( { + "level": 2, "name": f"_COMMAND_HISTORY.CommandBucket_Command_{cmd_index}", "address": bucket_cmd.vol.offset, "data": bucket_cmd.get_command(), @@ -281,7 +296,7 @@ def _generator( if command_history and command_history_properties: for command_history_property in command_history_properties: yield ( - 0, + command_history_property["level"], ( proc.UniqueProcessId, process_name, diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 0ef9e990a7..3b87734e74 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -445,6 +445,15 @@ def get_console_info( ) console_properties.append( { + "level": 0, + "name": "_CONSOLE_INFORMATION", + "address": console_info.vol.offset, + "data": "", + } + ) + console_properties.append( + { + "level": 1, "name": "_CONSOLE_INFORMATION.ScreenX", "address": console_info.ScreenX.vol.offset, "data": console_info.ScreenX, @@ -452,6 +461,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.ScreenY", "address": console_info.ScreenY.vol.offset, "data": console_info.ScreenY, @@ -459,6 +469,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.CommandHistorySize", "address": console_info.CommandHistorySize.vol.offset, "data": console_info.CommandHistorySize, @@ -466,6 +477,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.HistoryBufferCount", "address": console_info.HistoryBufferCount.vol.offset, "data": console_info.HistoryBufferCount, @@ -473,6 +485,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.HistoryBufferMax", "address": console_info.HistoryBufferMax.vol.offset, "data": console_info.HistoryBufferMax, @@ -480,6 +493,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.Title", "address": console_info.Title.vol.offset, "data": console_info.get_title(), @@ -487,6 +501,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.OriginalTitle", "address": console_info.OriginalTitle.vol.offset, "data": console_info.get_original_title(), @@ -498,6 +513,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.ProcessCount", "address": console_info.ProcessCount.vol.offset, "data": console_info.ProcessCount, @@ -505,6 +521,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.ConsoleProcessList", "address": console_info.ConsoleProcessList.vol.offset, "data": "", @@ -515,6 +532,7 @@ def get_console_info( ): console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}", "address": attached_proc.ConsoleProcess.dereference().vol.offset, "data": "", @@ -522,6 +540,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessId", "address": attached_proc.ConsoleProcess.ProcessId.vol.offset, "data": attached_proc.ConsoleProcess.ProcessId, @@ -529,6 +548,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ConsoleProcessList.ConsoleProcess_{index}_ProcessHandle", "address": attached_proc.ConsoleProcess.ProcessHandle.vol.offset, "data": hex( @@ -542,6 +562,7 @@ def get_console_info( ) console_properties.append( { + "level": 1, "name": "_CONSOLE_INFORMATION.HistoryList", "address": console_info.HistoryList.vol.offset, "data": "", @@ -553,6 +574,7 @@ def get_console_info( try: console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}", "address": command_history.vol.offset, "data": "", @@ -560,6 +582,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Application", "address": command_history.Application.vol.offset, "data": command_history.get_application(), @@ -567,6 +590,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_ProcessHandle", "address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset, "data": hex( @@ -576,6 +600,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_CommandCount", "address": None, "data": command_history.CommandCount, @@ -583,6 +608,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_LastDisplayed", "address": command_history.LastDisplayed.vol.offset, "data": command_history.LastDisplayed, @@ -593,8 +619,8 @@ def get_console_info( bucket_cmd, ) in command_history.get_commands(): try: - console_properties.append( - { + console_properties.append({ + "level": 3, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Command_{cmd_index}", "address": bucket_cmd.vol.offset, "data": bucket_cmd.get_command(), @@ -610,12 +636,21 @@ def get_console_info( ) vollog.debug(f"Getting ScreenBuffer entries for {console_info}") + console_properties.append( + { + "level": 1, + "name": "_CONSOLE_INFORMATION.CurrentScreenBuffer", + "address": console_info.CurrentScreenBuffer.vol.offset, + "data": "", + } + ) for screen_index, screen_info in enumerate( console_info.get_screens() ): try: console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}", "address": screen_info, "data": "", @@ -623,6 +658,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenX", "address": None, "data": screen_info.ScreenX, @@ -630,6 +666,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenY", "address": None, "data": screen_info.ScreenY, @@ -637,6 +674,7 @@ def get_console_info( ) console_properties.append( { + "level": 2, "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.Dump", "address": None, "data": "\n".join(screen_info.get_buffer()), @@ -748,7 +786,7 @@ def _generator( if console_info and console_properties: for console_property in console_properties: yield ( - 0, + console_property["level"], ( proc.UniqueProcessId, process_name, From a3c50e8221ed39e399b500376cb448019f9403db Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sat, 3 Aug 2024 14:27:42 -0700 Subject: [PATCH 035/250] #816 - black fixes --- .../framework/plugins/windows/consoles.py | 3 ++- .../symbols/windows/extensions/consoles.py | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 3b87734e74..9b41b2890b 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -619,7 +619,8 @@ def get_console_info( bucket_cmd, ) in command_history.get_commands(): try: - console_properties.append({ + console_properties.append( + { "level": 3, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Command_{cmd_index}", "address": bucket_cmd.vol.offset, diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 74ea80e847..23670bb9b3 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -23,9 +23,9 @@ def _valid_dbcs(self, c): 0x50, 0x60, 0x80, - 0xa8, - 0xc0, - 0xc8, + 0xA8, + 0xC0, + 0xC8, 0x98, 0xF8, 0xF0, @@ -226,7 +226,12 @@ class COMMAND(objects.StructType): """A Command Structure""" def is_valid(self): - if self.Length < 1 or self.Allocated < 1 or self.Length > 1024 or self.Allocated > 1024: + if ( + self.Length < 1 + or self.Allocated < 1 + or self.Length > 1024 + or self.Allocated > 1024 + ): return False return True @@ -256,7 +261,7 @@ def CommandCount(self): @property def ProcessHandle(self): - """ Allow ProcessHandle to be referenced regardless of OS version """ + """Allow ProcessHandle to be referenced regardless of OS version""" return self.ConsoleProcessHandle.ProcessHandle def is_valid(self, max_history=50): @@ -269,7 +274,11 @@ def is_valid(self, max_history=50): return False # Process handle must be a valid pid - if self.ProcessHandle <= 0 or self.ProcessHandle > 0xFFFF or self.ProcessHandle % 4 != 0: + if ( + self.ProcessHandle <= 0 + or self.ProcessHandle > 0xFFFF + or self.ProcessHandle % 4 != 0 + ): return False return True From 60f14478e2860cb97ef15954f80ee82202f5935f Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sat, 3 Aug 2024 14:30:27 -0700 Subject: [PATCH 036/250] #816 - black fixes --- .../framework/plugins/windows/cmdscan.py | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 1e435c56fd..b92d7efabc 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -65,7 +65,9 @@ def get_requirements(cls): @classmethod def get_filtered_vads( - cls, conhost_proc: interfaces.context.ContextInterface, size_filter: Optional[int]=0x40000000 + cls, + conhost_proc: interfaces.context.ContextInterface, + size_filter: Optional[int] = 0x40000000, ) -> List[Tuple[int, int]]: """ Returns vads of a process with smaller than size_filter @@ -129,7 +131,9 @@ def get_command_history( f"Found conhost process {conhost_proc} with pid {conhost_proc.UniqueProcessId}" ) - conhostexe_base, conhostexe_size = consoles.Consoles.find_conhostexe(conhost_proc) + conhostexe_base, conhostexe_size = consoles.Consoles.find_conhostexe( + conhost_proc + ) if not conhostexe_base: vollog.info( "Unable to find the location of conhost.exe. Analysis cannot proceed." @@ -154,7 +158,7 @@ def get_command_history( context, scanners.BytesScanner(max_history_bytes), sections=sections, - ): + ): command_history_properties = [] try: @@ -272,18 +276,24 @@ def _generator( no_registry = self.config.get("no_registry") if no_registry is False: - max_history, _max_buffers = consoles.Consoles.get_console_settings_from_registry( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - max_history, - [], + max_history, _max_buffers = ( + consoles.Consoles.get_console_settings_from_registry( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + max_history, + [], + ) ) vollog.debug(f"Possible CommandHistorySize values: {max_history}") - for proc, command_history, command_history_properties in self.get_command_history( + for ( + proc, + command_history, + command_history_properties, + ) in self.get_command_history( self.context, kernel.layer_name, kernel.symbol_table_name, @@ -305,7 +315,9 @@ def _generator( ( renderers.NotApplicableValue() if command_history_property["address"] is None - else format_hints.Hex(command_history_property["address"]) + else format_hints.Hex( + command_history_property["address"] + ) ), str(command_history_property["data"]), ), From 3e75c2ae9d29084485f6d2803c64d2c42e064ee0 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sun, 4 Aug 2024 14:40:45 +1000 Subject: [PATCH 037/250] Fix @functools.cache . It's available since Python 3.9 --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 300ab2ed0c..2d48f677b7 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1965,7 +1965,7 @@ def i_pages(self): class page(objects.StructType): @property - @functools.cache + @functools.lru_cache() def pageflags_enum(self) -> Dict: """Returns 'pageflags' enumeration key/values From f8c25176f3cc07e2c0a3688b26166a92f386fb16 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sun, 4 Aug 2024 09:30:37 -0700 Subject: [PATCH 038/250] #816 - add 17763, 18362, 19041 x64 support --- .../framework/plugins/windows/consoles.py | 15 +- .../consoles-win10-17763-3232-x64.json | 595 ++++++++++++++++++ .../consoles/consoles-win10-18362-x64.json | 585 +++++++++++++++++ .../consoles/consoles-win10-19041-x64.json | 585 +++++++++++++++++ 4 files changed, 1774 insertions(+), 6 deletions(-) create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 9b41b2890b..c9e8522a49 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -174,7 +174,7 @@ def determine_conhost_version( ) except: # unsure what to raise here. Also, it might be useful to add some kind of fallback, - # either to a user-provided version or to another method to determine tcpip.sys's version + # either to a user-provided version or to another method to determine conhost.exe's version raise exceptions.VolatilityException( "Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!" ) @@ -203,7 +203,10 @@ def determine_conhost_version( version_dict = {} else: version_dict = { - (10, 0, 17763, 0): "consoles-win10-17763-x64", + (10, 0, 17763, 1): "consoles-win10-17763-x64", + (10, 0, 17763, 3232): "consoles-win10-17763-3232-x64", + (10, 0, 18362, 0): "consoles-win10-18362-x64", + (10, 0, 19041, 0): "consoles-win10-19041-x64", (10, 0, 20348, 1): "consoles-win10-20348-x64", (10, 0, 20348, 1970): "consoles-win10-20348-1970-x64", (10, 0, 20348, 2461): "consoles-win10-20348-2461-x64", @@ -280,11 +283,11 @@ def determine_conhost_version( # try to grab the latest supported version of the current image NT version. If that symbol # version does not work, support has to be added manually. current_versions = [ - (nt_maj, nt_min, vers_min, tcpip_ver) - for nt_maj, nt_min, vers_min, tcpip_ver in version_dict + (nt_maj, nt_min, vers_min, conhost_ver) + for nt_maj, nt_min, vers_min, conhost_ver in version_dict if nt_maj == nt_major_version and nt_min == nt_minor_version - and tcpip_ver <= conhost_mod_version + and conhost_ver <= conhost_mod_version ] current_versions.sort() @@ -319,7 +322,7 @@ def create_conhost_symbol_table( nt_symbol_table: str, config_path: str, ) -> str: - """Creates a symbol table for TCP Listeners and TCP/UDP Endpoints. + """Creates a symbol table for conhost structures. Args: context: The context to retrieve required elements (layers, symbol tables) from 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 new file mode 100644 index 0000000000..ba17645c4d --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-3232-x64.json @@ -0,0 +1,595 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 34 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 144 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 148 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1736 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1672 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1800 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1384 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1360 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1368 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -760 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -752 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1392 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 48 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 58 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 112 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json new file mode 100644 index 0000000000..3c1fde26e5 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json @@ -0,0 +1,585 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 34 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 144 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 148 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1744 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1680 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1808 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1392 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1368 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1376 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -768 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -760 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1436 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 58 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 88 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json new file mode 100644 index 0000000000..39ca5f0eb2 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json @@ -0,0 +1,585 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 34 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 144 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 148 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1752 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1688 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1816 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1392 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1368 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1376 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -352 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -344 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1436 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 32 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 58 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 88 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} From 9b9535d8d716599630ab4c09b92b7e331a832d8b Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sun, 4 Aug 2024 10:14:36 -0700 Subject: [PATCH 039/250] #816 - other 20348 symbols --- .../consoles-win10-20348-1970-x64.json | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json 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 new file mode 100644 index 0000000000..e0935b4be1 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-1970-x64.json @@ -0,0 +1,595 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 24 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 26 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 136 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 140 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1616 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1552 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1680 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1296 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1272 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1280 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 9320 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 9328 + }, + "ExeAliasList": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2410 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "BufferDeque": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_DEQUE" + } + }, + "offset": 0 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "name": "void", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 16 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 40 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 48 + }, + "BufferEnd": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 54 + }, + "BufferLastIndex": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 56 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 96 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 8 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 64 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 88 + } }, + "kind": "struct", + "size": 96 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} From 8d6fd3cd78f0fadd223ae93a70a268048b4ccfe9 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Mon, 5 Aug 2024 14:28:43 +0200 Subject: [PATCH 040/250] Moved get_inode_metadata, separated inode and FD processing, error handling precision --- volatility3/framework/plugins/linux/lsof.py | 65 ++++++++++++------- .../framework/symbols/linux/__init__.py | 26 +------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index fa9d2bf610..167556e7d3 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -6,7 +6,7 @@ import logging, datetime from typing import List, Callable -from volatility3.framework import renderers, interfaces, constants +from volatility3.framework import renderers, interfaces, constants, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -46,7 +46,30 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] @classmethod - def list_fds_and_inodes( + def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface): + try: + dentry = filp.get_dentry() + if dentry: + inode_object = dentry.d_inode + if inode_object and inode_object.is_valid(): + itype = ( + inode_object.get_inode_type() or renderers.NotAvailableValue() + ) + return ( + inode_object.i_ino, + itype, + inode_object.i_size, + inode_object.get_file_mode(), + inode_object.get_change_time(), + inode_object.get_modification_time(), + inode_object.get_access_time(), + ) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning(f"Can't get inode metadata: {e}") + return tuple(renderers.NotAvailableValue() for _ in range(7)) + + @classmethod + def list_fds( cls, context: interfaces.context.ContextInterface, symbol_table: str, @@ -67,26 +90,27 @@ def list_fds_and_inodes( ) for fd_fields in fd_generator: - fd_num, filp, full_path = fd_fields - inode_metadata = linux.LinuxUtilities.get_inode_metadata(context, filp) - try: - inode_num, itype, file_size, imode, ctime, mtime, atime = next( - inode_metadata - ) - except Exception as e: - vollog.warning( - f"Can't get inode metadata for file descriptor {fd_num}: {e}" - ) - inode_num = itype = file_size = imode = ctime = mtime = atime = ( - renderers.NotAvailableValue() - ) - yield pid, task_comm, task, fd_num, filp, full_path, inode_num, itype, imode, ctime, mtime, atime, file_size + yield pid, task_comm, task, fd_fields + + @classmethod + def list_fds_and_inodes( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + filter_func: Callable[[int], bool] = lambda _: False, + ): + for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds( + context, symbol_table, filter_func + ): + inode_metadata = cls.get_inode_metadata(filp) + yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata def _generator(self, pids, symbol_table): filter_func = pslist.PsList.create_pid_filter(pids) fds_generator = self.list_fds_and_inodes( self.context, symbol_table, filter_func=filter_func ) + for ( pid, task_comm, @@ -94,14 +118,9 @@ def _generator(self, pids, symbol_table): fd_num, filp, full_path, - inode_num, - itype, - imode, - ctime, - mtime, - atime, - file_size, + inode_metadata, ) in fds_generator: + inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata fields = ( pid, task_comm, diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index d52c43dae3..03353135d0 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,8 +1,8 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # from typing import Iterator, List, Tuple, Optional, Union -import datetime, stat + from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility @@ -67,7 +67,7 @@ def __init__(self, *args, **kwargs) -> None: class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 2, 0) + _version = (2, 1, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -274,26 +274,6 @@ def files_descriptors_for_process( yield fd_num, filp, full_path - @classmethod - def get_inode_metadata(cls, context: interfaces.context.ContextInterface, filp): - """ - A helper function that gets the inodes metadata from a file descriptor - """ - dentry = filp.get_dentry() - if dentry: - inode_object = dentry.d_inode - if inode_object and inode_object.is_valid(): - itype = inode_object.get_inode_type() or "?" - yield ( - inode_object.i_ino, - itype, - inode_object.i_size, - inode_object.get_file_mode(), - inode_object.get_change_time(), - inode_object.get_modification_time(), - inode_object.get_access_time(), - ) - @classmethod def mask_mods_list( cls, From c9eb81c95fa530c58e493d77cde506f001e9e4f3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:40:48 -0700 Subject: [PATCH 041/250] PR review fixes: Improve eBPF extension objects: bpf_prog and added bpf_prog_aux. Apply changes to the EBPF and Sockstat plugins. --- volatility3/framework/plugins/linux/ebpf.py | 15 ++---- .../framework/plugins/linux/sockstat.py | 14 +++--- .../framework/symbols/linux/__init__.py | 1 + .../symbols/linux/extensions/__init__.py | 48 +++++++++++++++++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 33ba71fafc..8df506b069 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -1,12 +1,10 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import binascii import logging from typing import List from volatility3.framework import renderers, interfaces, exceptions -from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.interfaces import plugins from volatility3.framework.configuration import requirements @@ -53,18 +51,11 @@ def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] - bpf_prog_types = vmlinux.get_enumeration("bpf_prog_type") for prog in self.get_ebpf_programs(vmlinux): prog_addr = prog.vol.offset - prog_type = bpf_prog_types.lookup(prog.type) - prog_tag_addr = prog.tag.vol.offset - prog_tag_size = prog.tag.count - prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) - prog_tag = binascii.hexlify(prog_tag_bytes).decode() - prog_name = ( - utility.array_to_string(prog.aux.name) or renderers.NotAvailableValue() - ) + prog_type = prog.get_type() or renderers.NotAvailableValue() + prog_tag = prog.get_tag() or renderers.NotAvailableValue() + prog_name = prog.get_name() or renderers.NotAvailableValue() fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type) yield (0, fields) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index 78217fbecf..b0503b1052 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -151,17 +151,15 @@ def _extract_socket_filter_info( bpfprog = sock_filter.prog - BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter - try: - bpfprog_type = bpfprog.get_type() - if bpfprog_type == BPF_PROG_TYPE_UNSPEC: - return # cBPF filter - except AttributeError: + bpfprog_type = bpfprog.get_type() + if not bpfprog_type: # kernel < 3.18.140, it's a cBPF filter return None - BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter - if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER: + if bpfprog_type == "BPF_PROG_TYPE_UNSPEC": + return None # cBPF filter + + if bpfprog_type != "BPF_PROG_TYPE_SOCKET_FILTER": socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})" vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket") return None diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 248cb8d751..7a87135ff4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -38,6 +38,7 @@ def __init__(self, *args, **kwargs) -> None: # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) self.optional_set_type_class("bpf_prog", extensions.bpf_prog) + self.optional_set_type_class("bpf_prog_aux", extensions.bpf_prog_aux) self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2d48f677b7..2b971fb797 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -5,6 +5,7 @@ import collections.abc import logging import functools +import binascii import stat from datetime import datetime import socket as socket_module @@ -1607,20 +1608,59 @@ def get_state(self): class bpf_prog(objects.StructType): - def get_type(self): + def get_type(self) -> Union[str, None]: + """Returns a string with the eBPF program type""" + # The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to # 4.1.52 before it was moved to `bpf_prog::type` if self.has_member("type"): # kernel >= 4.1.52 - return self.type + return self.type.description if self.has_member("aux") and self.aux: if self.aux.has_member("prog_type"): # 3.18.140 <= kernel < 4.1.52 - return self.aux.prog_type + return self.aux.prog_type.description # kernel < 3.18.140 - raise AttributeError("Unable to find the BPF type") + return None + + def get_tag(self) -> Union[str, None]: + """Returns a string with the eBPF program tag""" + # 'tag' was added in kernels 4.10 + if not self.has_member("tag"): + return None + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] + + prog_tag_addr = self.tag.vol.offset + prog_tag_size = self.tag.count + prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size) + + prog_tag = binascii.hexlify(prog_tag_bytes).decode() + return prog_tag + + def get_name(self) -> Union[str, None]: + """Returns a string with the eBPF program name""" + if not self.has_member("aux"): + # 'prog_aux' was added in kernels 3.18 + return None + + return self.aux.get_name() + + +class bpf_prog_aux(objects.StructType): + def get_name(self) -> Union[str, None]: + """Returns a string with the eBPF program name""" + if not self.has_member("name"): + # 'name' was added in kernels 4.15 + return None + + if not self.name: + return None + + return utility.array_to_string(self.name) class cred(objects.StructType): From bd37aa3930c056fc8511969592f731a29e253c84 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:48:49 -0700 Subject: [PATCH 042/250] PR review fixes: Fix pidhashtable plugin --- volatility3/framework/plugins/linux/pidhashtable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index b24c73e777..ef110bdf19 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -191,7 +191,7 @@ def _determine_pid_func(self): "numbers" ) # kernels >= 2.6.24 - has_pid_numbers = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + has_pid_chain = self.vmlinux.has_type("upid") and self.vmlinux.get_type( "upid" ).has_member( "pid_chain" @@ -205,7 +205,7 @@ def _determine_pid_func(self): if pid_idr: # kernels >= 4.15 return self._pid_namespace_idr - elif pid_hash and has_pid_numbers and has_pid_numbers: + elif pid_hash and has_pid_numbers and has_pid_numbers and has_pid_chain: # 2.6.24 <= kernels < 4.15 return self._pid_hash_implementation From 7df0636f30d47f1d5b9373671aac410549474766 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 04:57:33 -0700 Subject: [PATCH 043/250] PR review fixes: Fix pidhashtable plugin explicit returns mixed with implicit returns --- volatility3/framework/plugins/linux/pidhashtable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index ef110bdf19..73cdff4521 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -50,19 +50,19 @@ def __init__(self, *args, **kwargs): self.vmlinux_layer = None def _is_valid_task(self, task): - return task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent) + return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): # The pid_type enumeration is present since 2.5.37, just in case pid_type_enum = self.vmlinux.get_enumeration("pid_type") if not pid_type_enum: vollog.error("Cannot find pid_type enum. Unsupported kernel") - return + return None pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID") if pidtype_pid is None: vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel") - return + return None # Typically PIDTYPE_PID = 0 return pidtype_pid @@ -154,7 +154,7 @@ def _task_for_radix_pid_node(self, nodep): pid_tasks_0 = pid.tasks[pidtype_pid].first if not pid_tasks_0: - return + return None task_struct_type = self.vmlinux.get_type("task_struct") if task_struct_type.has_member("pids"): From 1d43eb305d993a1306cc3971333d8a28bfee03af Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Tue, 6 Aug 2024 15:18:49 -0700 Subject: [PATCH 044/250] 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 ba17645c4d..0a8cb57829 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 f11fb39b19..2b98945c98 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 3c1fde26e5..3d76d1cb4e 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 39ca5f0eb2..681113b0f4 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 e0935b4be1..7bc6ae9d11 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 a4adf8028f..f612c527aa 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 045/250] #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 5cc7ab6f58..083a966a09 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" From 339a9a94f57adb2039984facf0cd22df0ba4bf90 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:00:09 -0700 Subject: [PATCH 046/250] PR review fixes: pidhashtable plugin add missing typing. --- volatility3/framework/plugins/linux/pidhashtable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 73cdff4521..3c429dc2fe 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -49,7 +49,7 @@ def __init__(self, *args, **kwargs): self.vmlinux = None self.vmlinux_layer = None - def _is_valid_task(self, task): + def _is_valid_task(self, task) -> bool: return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): From c8cb4465da3d71a879b47d981fdd4871afd9d521 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:08:21 -0700 Subject: [PATCH 047/250] PR review fixes: Remove filter function, it isn't needed --- volatility3/framework/plugins/linux/mountinfo.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 319c92cca1..dfb2e2f523 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -254,10 +254,7 @@ def get_superblocks(self): super_block: Kernel's struct super_block object """ # No filter so that we get all the mount namespaces from all tasks - pid_filter = pslist.PsList.create_pid_filter() - tasks = pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=pid_filter - ) + tasks = pslist.PsList.list_tasks(self.context, self.config["kernel"]) seen_sb_ptr = set() for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): From f737b88d03d9be9f4f0a9b43a83e02a88a620068 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 6 Aug 2024 23:12:01 -0700 Subject: [PATCH 048/250] PR review fixes: Improve _walk_dentry() and get_inodes() variable names, arguments and return values --- .../framework/plugins/linux/pagecache.py | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index c36f8a339d..cf09f23d81 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import logging import datetime from dataclasses import dataclass, astuple -from typing import List +from typing import List, Set from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -153,7 +153,23 @@ def _follow_symlink(inode, symlink_path) -> str: return symlink_path @classmethod - def _walk_dentry(cls, seen_dentries, root_dentry, parent): + def _walk_dentry( + cls, + seen_dentries: Set[int], + root_dentry: interfaces.objects.ObjectInterface, + parent_dir: str, + ): + """Walk dentries recursively + + Args: + seen_dentries: A set to ensure each dentry is processed only once + root_dentry: Root dentry object + parent_dir: Parent directory path + + Yields: + file_path: Filename including path + dentry: Dentry object + """ for dentry in root_dentry.get_subdirs(): dentry_addr = dentry.vol.offset @@ -173,19 +189,16 @@ def _walk_dentry(cls, seen_dentries, root_dentry, parent): # This allows us to have consistent paths if dentry.d_name.name: - name = dentry.d_name.name_as_str() + basename = dentry.d_name.name_as_str() # Do NOT use os.path.join() below - new_file = parent + "/" + name + file_path = parent_dir + "/" + basename else: continue - yield new_file, dentry, dentry.d_parent.vol.offset + yield file_path, dentry if inode.is_dir: - for new_file, dentry, parent_address in cls._walk_dentry( - seen_dentries, dentry, new_file - ): - yield new_file, dentry, parent_address + yield from cls._walk_dentry(seen_dentries, dentry, parent_dir=file_path) @classmethod def get_inodes( @@ -211,13 +224,15 @@ def get_inodes( seen_inodes = set() seen_dentries = set() for superblock, mountpoint in superblocks_iter: - parent = "" if mountpoint == "/" else mountpoint + parent_dir = "" if mountpoint == "/" else mountpoint # Superblock root dentry - root_dentry = superblock.s_root - if not root_dentry: + root_dentry_ptr = superblock.s_root + if not root_dentry_ptr: continue + root_dentry = root_dentry_ptr.dereference() + # Dentry sanity check if not root_dentry.is_root(): continue @@ -246,8 +261,8 @@ def get_inodes( yield inode_in # Children - for file_path, file_dentry, _ in cls._walk_dentry( - seen_dentries, root_dentry, parent + for file_path, file_dentry in cls._walk_dentry( + seen_dentries, root_dentry, parent_dir ): if not file_dentry: continue From 805b3514c3b14c53724cd38d722540d84949d51f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 7 Aug 2024 00:23:19 -0700 Subject: [PATCH 049/250] PR review fixes: Fix page flags list method name, this was introduced earlier in another commit of this PR. --- volatility3/framework/plugins/linux/pagecache.py | 2 +- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index cf09f23d81..fd62cc1e46 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -486,7 +486,7 @@ def _generator(self): page_index = int(page_obj.index) page_file_offset = page_index * vmlinux_layer.page_size dump_safe = page_file_offset < inode_size - page_flags_list = page_obj.get_flags() + page_flags_list = page_obj.get_flags_list() page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list]) fields = ( page_vaddr, diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2b971fb797..900cc9b6c2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2027,7 +2027,7 @@ def pageflags_enum(self) -> Dict: return pageflags_enum - def flags_list(self) -> List[str]: + def get_flags_list(self) -> List[str]: """Returns a list of page flags Returns: From 46842981fb25aa6f1b242ef5990642fdeeb0cf00 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 7 Aug 2024 00:24:58 -0700 Subject: [PATCH 050/250] PR review fixes: Use contextlib.suppress() instead of an empty exception handler --- volatility3/framework/symbols/linux/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 7a87135ff4..96bc56a187 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import math +import contextlib from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union @@ -676,15 +677,13 @@ def tag_internal_value(self) -> int: return self.RADIX_TREE_INTERNAL_NODE def get_tree_height(self, treep) -> int: - try: + with contextlib.suppress(exceptions.SymbolError): if self.vmlinux.get_type("radix_tree_root").has_member("height"): # kernels < 4.7.10 radix_tree_root = self.vmlinux.object( "radix_tree_root", offset=treep, absolute=True ) return radix_tree_root.height - except exceptions.SymbolError: - pass # kernels >= 4.7.10 return 0 From 10376a2687b8546df88bf6b3a3e11126f8816479 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Wed, 7 Aug 2024 10:38:29 +0200 Subject: [PATCH 051/250] use one class with flag --- README.md | 2 +- pyproject.toml | 2 +- volatility3/framework/__init__.py | 2 +- .../framework/plugins/windows/vadyarascan.py | 7 +- volatility3/framework/plugins/yarascan.py | 81 ++++++++----------- 5 files changed, 38 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 886790df8e..1463c2bde9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ more details. ## Requirements -Volatility 3 requires Python 3.7.3 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as: +Volatility 3 requires Python 3.8.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as: ```shell pip3 install -r requirements-minimal.txt diff --git a/pyproject.toml b/pyproject.toml index 207f762cc2..2e1636a437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" authors = [ { name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" }, ] -requires-python = ">=3.7.3" +requires-python = ">=3.8.0" license = { text = "VSL" } dynamic = ["dependencies", "optional-dependencies", "version"] diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 74db773cf8..51310bfa29 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -7,7 +7,7 @@ import sys import zipfile -required_python_version = (3, 7, 3) +required_python_version = (3, 8, 0) if ( sys.version_info.major != required_python_version[0] or sys.version_info.minor < required_python_version[1] diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 4a84a12854..7bc3377c38 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -73,8 +73,9 @@ def _generator(self): ) continue + data = layer.read(start, size, True) if not yarascan.YaraScan._yara_x: - for match in rules.match(data=layer.read(start, size, True)): + for match in rules.match(data=data): if yarascan.YaraScan.yara_returns_instances(): for match_string in match.strings: for instance in match_string.instances: @@ -95,9 +96,7 @@ def _generator(self): value, ) else: - data = layer.read(start, size, True) - results = rules.scan(data) - for match in results.matching_rules: + for match in rules.scan(data).matching_rules: for match_string in match.patterns: for instance in match_string.matches: yield 0, ( diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 6a4dd9251c..310bbd0725 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -36,7 +36,7 @@ raise -class BaseYaraScanner(interfaces.layers.ScannerInterface): +class YaraScanner(interfaces.layers.ScannerInterface): _version = (2, 1, 0) # yara.Rules isn't exposed, so we can't type this properly @@ -45,32 +45,44 @@ def __init__(self, rules) -> None: if rules is None: raise ValueError("No rules provided to YaraScanner") self._rules = rules - - -class YaraPythonScanner(BaseYaraScanner): - def __init__(self, rules) -> None: - super().__init__(rules) - self.st_object = not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3) + self.st_object = ( + None + if USE_YARA_X + else not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3) + ) def __call__( self, data: bytes, data_offset: int ) -> Iterable[Tuple[int, str, str, bytes]]: - for match in self._rules.match(data=data): - if YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: + if USE_YARA_X: + for match in self._rules.scan(data).matching_rules: + for match_string in match.patterns: + for instance in match_string.matches: yield ( instance.offset + data_offset, - match.rule, + f"{match.namespace}.{match.identifier}", match_string.identifier, - instance.matched_data, + data[instance.offset : instance.offset + instance.length], ) - else: - for offset, name, value in match.strings: - yield (offset + data_offset, match.rule, name, value) + else: + for match in self._rules.match(data=data): + if YaraScan.yara_returns_instances(): + for match_string in match.strings: + for instance in match_string.instances: + yield ( + instance.offset + data_offset, + match.rule, + match_string.identifier, + instance.matched_data, + ) + else: + for offset, name, value in match.strings: + yield (offset + data_offset, match.rule, name, value) @staticmethod def get_rule(rule): + if USE_YARA_X: + return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") return yara.compile( sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} ) @@ -78,47 +90,18 @@ def get_rule(rule): @staticmethod def from_compiled_file(filepath): with resources.ResourceAccessor().open(filepath, "rb") as fp: + if USE_YARA_X: + return yara_x.Rules.deserialize_from(file=fp) return yara.load(file=fp) @staticmethod def from_file(filepath): with resources.ResourceAccessor().open(filepath, "rb") as fp: + if USE_YARA_X: + return yara_x.compile(fp.read().decode()) return yara.compile(file=fp) -class YaraXScanner(BaseYaraScanner): - def __call__( - self, data: bytes, data_offset: int - ) -> Iterable[Tuple[int, str, str, bytes]]: - results = self._rules.scan(data) - for match in results.matching_rules: - for match_string in match.patterns: - for instance in match_string.matches: - yield ( - instance.offset + data_offset, - f"{match.namespace}.{match.identifier}", - match_string.identifier, - data[instance.offset : instance.offset + instance.length], - ) - - @staticmethod - def get_rule(rule): - return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}") - - @staticmethod - def from_compiled_file(filepath): - with resources.ResourceAccessor().open(filepath, "rb") as fp: - return yara_x.Rules.deserialize_from(file=fp) - - @staticmethod - def from_file(filepath): - with resources.ResourceAccessor().open(filepath, "rb") as fp: - return yara_x.compile(fp.read().decode()) - - -YaraScanner = YaraXScanner if USE_YARA_X else YaraPythonScanner - - class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" From de6637c9871ac16f1a5278c4831dfe60778852b9 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 00:52:34 -0700 Subject: [PATCH 052/250] PR review fixes: ebpf plugin code improvement. Use the object_from_symbol() instead --- volatility3/framework/plugins/linux/ebpf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 8df506b069..9d41c0ffbf 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -43,8 +43,7 @@ def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: "Cannot find the eBPF prog idr. Unsupported kernel" ) - prog_idr_addr = vmlinux.get_symbol("prog_idr").address - prog_idr = vmlinux.object("idr", offset=prog_idr_addr) + prog_idr = vmlinux.object_from_symbol("prog_idr") for page_addr in prog_idr.get_page_addresses(): bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) yield bpf_prog From ee10ba8abb7b8c932d9af85a37edefc3fd03fa63 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:46:53 -0700 Subject: [PATCH 053/250] PR review fixes: Fix IDR explicit returns mixed with implicit returns and improve and fix code. --- .../symbols/linux/extensions/__init__.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 900cc9b6c2..6eafd41736 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2134,7 +2134,7 @@ def idr_find(self, idr_id: int) -> int: """Finds an ID within the IDR data structure. Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11 Args: - idr_id: The IDR element ID + idr_id: The IDR lookup ID Returns: A pointer to the given ID element @@ -2144,28 +2144,28 @@ def idr_find(self, idr_id: int) -> int: vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" ) - return + return None if idr_id < 0: - return + return None - cur_layer = self.top - if not cur_layer: - return + idr_layer = self.top + if not idr_layer: + return None - n = (cur_layer.layer + 1) * self.IDR_BITS + n = (idr_layer.layer + 1) * self.IDR_BITS - if idr_id > self.idr_max(cur_layer.layer + 1): - return + if idr_id > self.idr_max(idr_layer.layer + 1): + return None assert n != 0 - while n > 0 and cur_layer: + while n > 0 and idr_layer: n -= self.IDR_BITS - assert n == cur_layer.layer * self.IDR_BITS - cur_layer = cur_layer.ary[(idr_id >> n) & self.IDR_MASK] + assert n == idr_layer.layer * self.IDR_BITS + idr_layer = idr_layer.ary[(idr_id >> n) & self.IDR_MASK] - return cur_layer.v() + return idr_layer def _old_kernel_get_page_addresses(self, in_use) -> int: # Kernels < 4.11 From 0f3f33863370f68b7da3069db5b29282dabab932 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:52:57 -0700 Subject: [PATCH 054/250] PR review fixes: Since the IDR, XArray and RadixTree can store any value, it renames the function names to a more generic name --- volatility3/framework/plugins/linux/ebpf.py | 2 +- .../framework/plugins/linux/pidhashtable.py | 2 +- volatility3/framework/symbols/linux/__init__.py | 4 ++-- .../symbols/linux/extensions/__init__.py | 16 +++++++++------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 9d41c0ffbf..70082daf53 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -44,7 +44,7 @@ def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: ) prog_idr = vmlinux.object_from_symbol("prog_idr") - for page_addr in prog_idr.get_page_addresses(): + for page_addr in prog_idr.get_entries(): bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True) yield bpf_prog diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 3c429dc2fe..c384cb5cda 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -175,7 +175,7 @@ def _pid_namespace_idr(self): ns_addr = self.vmlinux.get_symbol("init_pid_ns").address ns = self.vmlinux.object("pid_namespace", offset=ns_addr) - for page_addr in ns.idr.get_page_addresses(): + for page_addr in ns.idr.get_entries(): task = self._task_for_radix_pid_node(page_addr) if self._is_valid_task(task): yield task diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 96bc56a187..89f9702754 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -572,7 +572,7 @@ def _iter_node(self, nodep, height) -> int: for child_node in self._iter_node(nodep, height - 1): yield child_node - def get_page_addresses(self, root: interfaces.objects.ObjectInterface) -> int: + def get_entries(self, root: interfaces.objects.ObjectInterface) -> int: """Walks the tree data structure Args: @@ -762,7 +762,7 @@ def get_cached_pages(self) -> interfaces.objects.ObjectInterface: Page objects """ - for page_addr in self._tree.get_page_addresses(self._page_cache.i_pages): + for page_addr in self._tree.get_entries(self._page_cache.i_pages): if not page_addr: continue diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 6eafd41736..29af5c5101 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2167,7 +2167,7 @@ def idr_find(self, idr_id: int) -> int: return idr_layer - def _old_kernel_get_page_addresses(self, in_use) -> int: + def _old_kernel_get_entries(self) -> int: # Kernels < 4.11 total = next_id = 0 while total < in_use: @@ -2178,14 +2178,14 @@ def _old_kernel_get_page_addresses(self, in_use) -> int: next_id += 1 - def _new_kernel_get_page_addresses(self, _in_use) -> int: + def _new_kernel_get_entries(self) -> int: # Kernels >= 4.11 vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) - for page_addr in tree.get_page_addresses(root=self.idr_rt): + for page_addr in tree.get_entries(root=self.idr_rt): yield page_addr - def get_page_addresses(self, in_use=0) -> int: + def get_entries(self) -> int: """Walks the IDR and yield a pointer associated with each element. Args: @@ -2195,9 +2195,11 @@ def get_page_addresses(self, in_use=0) -> int: A pointer associated with each element. """ if self.has_member("idr_rt"): - get_page_addresses_func = self._new_kernel_get_page_addresses + # Kernels >= 4.11 + get_entries_func = self._new_kernel_get_entries else: - get_page_addresses_func = self._old_kernel_get_page_addresses + # Kernels < 4.11 + get_entries_func = self._old_kernel_get_entries - for page_addr in get_page_addresses_func(in_use): + for page_addr in get_entries_func(): yield page_addr From 7a8dea3356c709cda2bdd20b7cb3ff97f1f7987d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 01:57:35 -0700 Subject: [PATCH 055/250] PR review fixes: Code scanning complains about these unused variables. Let's comment them and adapt the FIXME message --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 29af5c5101..9b03235cba 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2063,10 +2063,9 @@ def to_paddr(self) -> int: vmemmap_start = vmemmap_base_l4 else: # 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5 - vmemmap_base_l5 = 0xFFD4000000000000 - vmemmap_start = vmemmap_base_l5 - - # FIXME: Remove this exception once 5-level paging is supported. + # FIXME: Once 5-level paging is supported, uncomment the following lines and remove the exception + # vmemmap_base_l5 = 0xFFD4000000000000 + # vmemmap_start = vmemmap_base_l5 raise exceptions.VolatilityException( "5-level paging is not yet supported" ) From 06508a4afba235812e7d1b7bfb79467adf2ed6fc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 02:00:27 -0700 Subject: [PATCH 056/250] PR review fixes: Fix the IDR's old kernel get_entries --- .../framework/symbols/linux/extensions/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 9b03235cba..f7b0df6be4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2168,11 +2168,12 @@ def idr_find(self, idr_id: int) -> int: def _old_kernel_get_entries(self) -> int: # Kernels < 4.11 + cur = self.cur total = next_id = 0 - while total < in_use: - page_addr = self.idr_find(next_id) - if page_addr: - yield page_addr + while next_id < cur: + entry = self.idr_find(next_id) + if entry: + yield entry total += 1 next_id += 1 From 2c85ea525e15c0cb745f3b806836f22ea44a4b4c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 8 Aug 2024 02:03:28 -0700 Subject: [PATCH 057/250] PR review fixes: Fix page extension object get_content() explicit returns mixed with implicit returns. --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f7b0df6be4..be4df6a13d 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2102,7 +2102,7 @@ def get_content(self) -> Union[str, None]: physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() if not page_paddr: - return + return None page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size) return page_data From f22575669a6ccd9afaeef126e81a6adad8b880f6 Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Fri, 9 Aug 2024 10:20:28 +0200 Subject: [PATCH 058/250] Modifications following the review --- volatility3/framework/plugins/linux/lsof.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 167556e7d3..9a0fd74178 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -66,7 +66,7 @@ def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface): ) except (exceptions.InvalidAddressException, AttributeError) as e: vollog.warning(f"Can't get inode metadata: {e}") - return tuple(renderers.NotAvailableValue() for _ in range(7)) + return None @classmethod def list_fds( @@ -103,6 +103,10 @@ def list_fds_and_inodes( context, symbol_table, filter_func ): inode_metadata = cls.get_inode_metadata(filp) + if inode_metadata is None: + inode_metadata = tuple( + interfaces.renderers.BaseAbsentValue() for _ in range(7) + ) yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata def _generator(self, pids, symbol_table): From 53f3d12341e722f1058c42436adfea600af94bab Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 9 Aug 2024 23:35:41 -0700 Subject: [PATCH 059/250] linuxutilities code improvement. Remove code duplication --- volatility3/framework/symbols/linux/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 89f9702754..90f5cc8a20 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -419,9 +419,7 @@ def get_module_from_volobj_type( Returns: A kernel object (vmlinux) """ - symbol_table_arr = volobj.vol.type_name.split("!", 1) - symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None - + symbol_table = volobj.get_symbol_table_name() module_names = context.modules.get_modules_by_symbol_tables(symbol_table) module_names = list(module_names) From 17861618df3744d572955b50b2b1b1ad1d0961e5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 10 Aug 2024 00:13:38 -0700 Subject: [PATCH 060/250] PR review fixes: Rename Tree to IDStorage. Move choose_id_storage() form LinuxUtilities to IDStorage. Use context and kernel_module_name instead of vmlinux --- .../framework/symbols/linux/__init__.py | 87 +++++++++++-------- .../symbols/linux/extensions/__init__.py | 14 +-- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 90f5cc8a20..632ac2f6be 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -431,17 +431,51 @@ def get_module_from_volobj_type( return kernel + +class IDStorage(ABC): + """Abstraction to support both XArray and RadixTree""" + + # Dynamic values, these will be initialized later + CHUNK_SHIFT = None + CHUNK_SIZE = None + CHUNK_MASK = None + + def __init__( + self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ): + self.vmlinux = context.modules[kernel_module_name] + self.vmlinux_layer = self.vmlinux.context.layers[self.vmlinux.layer_name] + + self.pointer_size = self.vmlinux.get_type("pointer").size + # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on + # the node.slots[] array size + node_type = self.vmlinux.get_type(self.node_type_name) + slots_array_size = node_type.child_template("slots").count + + # Calculate the LSB index - 1 + self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 + self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT + self.CHUNK_MASK = self.CHUNK_SIZE - 1 + @classmethod - def choose_kernel_tree(cls, vmlinux: interfaces.context.ModuleInterface) -> "Tree": - """Returns the appropriate tree data structure instance for the current kernel implementation. + def choose_id_storage( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ) -> "IDStorage": + """Returns the appropriate ID storage data structure instance for the current kernel implementation. This is used by the IDR and the PageCache to choose between the XArray and RadixTree. Args: - vmlinux: The kernel module object + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the kernel module on which to operate Returns: - The appropriate Tree instance for the current kernel + The appropriate ID storage instance for the current kernel """ + vmlinux = context.modules[kernel_module_name] address_space_type = vmlinux.get_type("address_space") address_space_has_i_pages = address_space_type.has_member("i_pages") i_pages_type_name = ( @@ -455,33 +489,9 @@ def choose_kernel_tree(cls, vmlinux: interfaces.context.ModuleInterface) -> "Tre ) and vmlinux.get_type("radix_tree_root").has_member("xa_head") if i_pages_is_xarray or i_pages_is_radix_tree_root: - return XArray(vmlinux) + return XArray(context, kernel_module_name) else: - return RadixTree(vmlinux) - - -class Tree(ABC): - """Abstraction to support both XArray and RadixTree""" - - # Dynamic values, these will be initialized later - CHUNK_SHIFT = None - CHUNK_SIZE = None - CHUNK_MASK = None - - def __init__(self, vmlinux: interfaces.context.ModuleInterface): - self.vmlinux = vmlinux - self.vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] - - self.pointer_size = self.vmlinux.get_type("pointer").size - # Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on - # the node.slots[] array size - node_type = self.vmlinux.get_type(self.node_type_name) - slots_array_size = node_type.child_template("slots").count - - # Calculate the LSB index - 1 - self.CHUNK_SHIFT = slots_array_size.bit_length() - 1 - self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT - self.CHUNK_MASK = self.CHUNK_SIZE - 1 + return RadixTree(context, kernel_module_name) @property @abstractmethod @@ -601,7 +611,7 @@ def get_entries(self, root: interfaces.objects.ObjectInterface) -> int: yield child_node -class XArray(Tree): +class XArray(IDStorage): XARRAY_TAG_MASK = 3 XARRAY_TAG_INTERNAL = 2 @@ -637,7 +647,7 @@ def is_valid_node(self, nodep) -> bool: return not self.is_node_tagged(nodep) -class RadixTree(Tree): +class RadixTree(IDStorage): RADIX_TREE_INTERNAL_NODE = 1 RADIX_TREE_EXCEPTIONAL_ENTRY = 2 RADIX_TREE_ENTRY_MASK = 3 @@ -741,17 +751,20 @@ class PageCache(object): def __init__( self, + context: interfaces.context.ContextInterface, + kernel_module_name: str, page_cache: interfaces.objects.ObjectInterface, - vmlinux: interfaces.context.ModuleInterface, ): """ Args: + context: interfaces.context.ContextInterface, + kernel_module_name: The name of the kernel module on which to operate page_cache: Page cache address space - vmlinux: Kernel module object """ - self.vmlinux = vmlinux + self.vmlinux = context.modules[kernel_module_name] + self._page_cache = page_cache - self._tree = LinuxUtilities.choose_kernel_tree(self.vmlinux) + self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name) def get_cached_pages(self) -> interfaces.objects.ObjectInterface: """Returns all page cache contents @@ -760,7 +773,7 @@ def get_cached_pages(self) -> interfaces.objects.ObjectInterface: Page objects """ - for page_addr in self._tree.get_entries(self._page_cache.i_pages): + for page_addr in self._idstorage.get_entries(self._page_cache.i_pages): if not page_addr: continue diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index be4df6a13d..d00af7a3f3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1972,8 +1972,11 @@ def get_pages(self) -> interfaces.objects.ObjectInterface: elif not (self.i_mapping and self.i_mapping.nrpages > 0): return - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - page_cache = linux.PageCache(self.i_mapping.dereference(), vmlinux) + page_cache = linux.PageCache( + context=self._context, + kernel_module_name="kernel", + page_cache=self.i_mapping.dereference(), + ) yield from page_cache.get_cached_pages() def get_contents(self): @@ -2180,9 +2183,10 @@ def _old_kernel_get_entries(self) -> int: def _new_kernel_get_entries(self) -> int: # Kernels >= 4.11 - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - tree = linux.LinuxUtilities.choose_kernel_tree(vmlinux) - for page_addr in tree.get_entries(root=self.idr_rt): + id_storage = linux.IDStorage.choose_id_storage( + self._context, kernel_module_name="kernel" + ) + for page_addr in id_storage.get_entries(root=self.idr_rt): yield page_addr def get_entries(self) -> int: From 8f9d565f6300750ad30c0aaca0aa01cfbcb1a417 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 10 Aug 2024 00:49:18 -0700 Subject: [PATCH 061/250] PR review fixes: Fix minor typo to match verb form from other docstrings --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index fd62cc1e46..f062655a6c 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -159,7 +159,7 @@ def _walk_dentry( root_dentry: interfaces.objects.ObjectInterface, parent_dir: str, ): - """Walk dentries recursively + """Walks dentries recursively Args: seen_dentries: A set to ensure each dentry is processed only once From f804b44ff6f8451e8f1091383b8d9c2300f7bfbb Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 13 Aug 2024 01:00:57 -0700 Subject: [PATCH 062/250] Fix test.yaml, it should remove *.bin and not *.lime. There is no *.lime atm. --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 55b2e4b602..668b814ce8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -46,7 +46,7 @@ jobs: - name: Clean up post-test run: | - rm -rf *.lime + rm -rf *.bin rm -rf *.img cd volatility3/symbols rm -rf linux From 7efa2210a550d55f356b6ebb35cafcdfd4a58d1f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 13 Aug 2024 01:05:44 -0700 Subject: [PATCH 063/250] PR review fixes: Adjust LinuxUtilities version since we moved choose_id_storage() back to the IDStorage class. --- volatility3/framework/plugins/linux/pidhashtable.py | 2 +- volatility3/framework/symbols/linux/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index c384cb5cda..9a2528543c 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -34,7 +34,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0) + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), requirements.BooleanRequirement( name="decorate_comm", diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 632ac2f6be..91abf7db4e 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -74,7 +74,7 @@ def __init__(self, *args, **kwargs) -> None: class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 2, 0) + _version = (2, 1, 1) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) From 87fe3444ebb0d3bf7595c6cefc24f04145ff8dae Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 14 Aug 2024 23:50:39 -0700 Subject: [PATCH 064/250] Linux: Add support for mount namespace in kernels 6.8+. This fixes volatilityfoundation/volatility3#1187 --- .../framework/symbols/linux/__init__.py | 33 +++++++++++++++ .../symbols/linux/extensions/__init__.py | 42 +++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 03353135d0..525492141a 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -425,3 +425,36 @@ def get_module_from_volobj_type( kernel = context.modules[kernel_module_name] return kernel + + +class RBTree(object): + """Simple Red-Black tree abstraction""" + + def __init__(self, root): + self.root = root + + def _walk_nodes(self, root_node) -> Iterator[int]: + """Traverses the Red-Black tree from the root node and yields a pointer to each + node in this tree. + + Args: + root_node: A Red-Black tree node from which to start descending + + Yields: + A pointer to every node descending from the specified root node + """ + if not root_node: + return + + yield root_node + yield from self._walk_nodes(root_node.rb_left) + yield from self._walk_nodes(root_node.rb_right) + + def get_nodes(self) -> Iterator[int]: + """Yields a pointer to each node in the Red-Black tree + + Yields: + A pointer to every node in the Red-Black tree + """ + + yield from self._walk_nodes(root_node=self.root.rb_node) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 05679523f3..645f493cb6 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1299,14 +1299,42 @@ def get_inode(self): else: raise AttributeError("Unable to find mnt_namespace inode") - def get_mount_points(self): + def get_mount_points( + self, + ) -> Iterator[interfaces.objects.ObjectInterface]: + """Yields the mount points for this mount namespace. + + Yields: + mount struct instances + """ table_name = self.vol.type_name.split(constants.BANG)[0] - mnt_type = table_name + constants.BANG + "mount" - if not self._context.symbol_space.has_type(mnt_type): - # Old kernels ~ 2.6 - mnt_type = table_name + constants.BANG + "vfsmount" - for mount in self.list.to_list(mnt_type, "mnt_list"): - yield mount + + if self.has_member("list"): + # kernels < 6.8 + mnt_type = table_name + constants.BANG + "mount" + if not self._context.symbol_space.has_type(mnt_type): + # In kernels < 3.3, the 'mount' struct didn't exist, and the 'mnt_list' + # member was part of the 'vfsmount' struct. + mnt_type = table_name + constants.BANG + "vfsmount" + + yield from self.list.to_list(mnt_type, "mnt_list") + elif ( + self.has_member("mounts") + and self.mounts.vol.type_name == table_name + constants.BANG + "rb_root" + ): + # kernels >= 6.8 + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type( + self._context, self + ) + for node in linux.RBTree(self.mounts).get_nodes(): + mnt = linux.LinuxUtilities.container_of( + node, "mount", "mnt_list", vmlinux + ) + yield mnt + else: + raise exceptions.VolatilityException( + "Unsupported kernel mount namespace implementation" + ) class net(objects.StructType): From 4513806a7ec7f8c4da746a7ffde952d9f170a07f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 15 Aug 2024 07:55:45 -0700 Subject: [PATCH 065/250] Move RBTree to the rb_root extension object for better integration --- .../framework/symbols/linux/__init__.py | 34 +------------------ .../symbols/linux/extensions/__init__.py | 30 +++++++++++++++- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 525492141a..c2eb3d7912 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -46,6 +46,7 @@ def __init__(self, *args, **kwargs) -> None: # Might not exist in older kernels or the current symbols self.optional_set_type_class("mount", extensions.mount) self.optional_set_type_class("mnt_namespace", extensions.mnt_namespace) + self.optional_set_type_class("rb_root", extensions.rb_root) # Network self.set_type_class("net", extensions.net) @@ -425,36 +426,3 @@ def get_module_from_volobj_type( kernel = context.modules[kernel_module_name] return kernel - - -class RBTree(object): - """Simple Red-Black tree abstraction""" - - def __init__(self, root): - self.root = root - - def _walk_nodes(self, root_node) -> Iterator[int]: - """Traverses the Red-Black tree from the root node and yields a pointer to each - node in this tree. - - Args: - root_node: A Red-Black tree node from which to start descending - - Yields: - A pointer to every node descending from the specified root node - """ - if not root_node: - return - - yield root_node - yield from self._walk_nodes(root_node.rb_left) - yield from self._walk_nodes(root_node.rb_right) - - def get_nodes(self) -> Iterator[int]: - """Yields a pointer to each node in the Red-Black tree - - Yields: - A pointer to every node in the Red-Black tree - """ - - yield from self._walk_nodes(root_node=self.root.rb_node) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 645f493cb6..c18cd093d9 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1326,7 +1326,7 @@ def get_mount_points( vmlinux = linux.LinuxUtilities.get_module_from_volobj_type( self._context, self ) - for node in linux.RBTree(self.mounts).get_nodes(): + for node in self.mounts.get_nodes(): mnt = linux.LinuxUtilities.container_of( node, "mount", "mnt_list", vmlinux ) @@ -1925,3 +1925,31 @@ def get_file_mode(self) -> str: The inode's file mode string """ return stat.filemode(self.i_mode) + + +class rb_root(objects.StructType): + def _walk_nodes(self, root_node) -> Iterator[int]: + """Traverses the Red-Black tree from the root node and yields a pointer to each + node in this tree. + + Args: + root_node: A Red-Black tree node from which to start descending + + Yields: + A pointer to every node descending from the specified root node + """ + if not root_node: + return + + yield root_node + yield from self._walk_nodes(root_node.rb_left) + yield from self._walk_nodes(root_node.rb_right) + + def get_nodes(self) -> Iterator[int]: + """Yields a pointer to each node in the Red-Black tree + + Yields: + A pointer to every node in the Red-Black tree + """ + + yield from self._walk_nodes(root_node=self.rb_node) From 8d7edfdca6fa84983b4ee734e3f45a9d07c28ff1 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Wed, 21 Aug 2024 20:36:09 +0100 Subject: [PATCH 066/250] Bump as the release branch has been cut --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index f219fb0af2..4df0b90414 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change VERSION_MINOR = 8 # Number of changes that only add to the interface -VERSION_PATCH = 0 # Number of changes that do not change the interface +VERSION_PATCH = 1 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( From 71cdca5883b234680773e000a663750964d4860e Mon Sep 17 00:00:00 2001 From: k1nd0ne Date: Thu, 22 Aug 2024 16:45:04 +0200 Subject: [PATCH 067/250] Updating version + docstring --- volatility3/framework/plugins/linux/lsof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 9a0fd74178..360f897490 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -18,10 +18,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): - """Lists all memory maps for all processes.""" + """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (1, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From feae6a9aa0f5c7869174e0b906ec83e61b3e14e3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 00:42:52 +1000 Subject: [PATCH 068/250] PR review fixes: Use plugin's open method instead of the builtin open() --- volatility3/framework/plugins/linux/pagecache.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index f062655a6c..cf4151c852 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import logging import datetime from dataclasses import dataclass, astuple -from typing import List, Set +from typing import List, Set, Type from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -408,6 +408,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] def write_inode_content_to_file( inode: interfaces.objects.ObjectInterface, filename: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], vmlinux_layer: interfaces.layers.TranslationLayerInterface, ) -> None: """Extracts the inode's contents from the page cache and saves them to a file @@ -415,6 +416,7 @@ def write_inode_content_to_file( Args: inode: The inode to dump filename: Filename for writing the inode content + open_method: class for constructing output files vmlinux_layer: The kernel layer to obtain the page size """ if not inode.is_reg: @@ -426,7 +428,7 @@ def write_inode_content_to_file( # Additionally, using the page index will guarantee that each page is written at the # appropriate file position. try: - with open(filename, "wb") as f: + with open_method(filename) as f: inode_size = inode.i_size f.truncate(inode_size) @@ -502,7 +504,7 @@ def _generator(self): if self.config["dump"]: filename = self.config["dump"] vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename) - self.write_inode_content_to_file(inode, filename, vmlinux_layer) + self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer) def run(self): headers = [ From 3c70c1b9f7c2251e35eea276f5d5099b68392a48 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 00:58:04 +1000 Subject: [PATCH 069/250] PR review fixes: Use context and module_name instead of vmlinux in ebpf plugin --- volatility3/framework/plugins/linux/ebpf.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/ebpf.py b/volatility3/framework/plugins/linux/ebpf.py index 70082daf53..2267dd9228 100644 --- a/volatility3/framework/plugins/linux/ebpf.py +++ b/volatility3/framework/plugins/linux/ebpf.py @@ -29,15 +29,21 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ), ] - def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: + def get_ebpf_programs( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> interfaces.objects.ObjectInterface: """Enumerate eBPF programs walking its IDR. Args: - vmlinux: The kernel symbols object - + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate Yields: eBPF program objects """ + vmlinux = context.modules[vmlinux_module_name] + if not vmlinux.has_symbol("prog_idr"): raise exceptions.VolatilityException( "Cannot find the eBPF prog idr. Unsupported kernel" @@ -49,8 +55,7 @@ def get_ebpf_programs(self, vmlinux) -> interfaces.objects.ObjectInterface: yield bpf_prog def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - for prog in self.get_ebpf_programs(vmlinux): + for prog in self.get_ebpf_programs(self.context, self.config["kernel"]): prog_addr = prog.vol.offset prog_type = prog.get_type() or renderers.NotAvailableValue() prog_tag = prog.get_tag() or renderers.NotAvailableValue() From 695635199e2bd3af11bee522e054719ee1898db6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 01:14:09 +1000 Subject: [PATCH 070/250] PR review fixes: Avoid saving state in the pidhashtable plugin --- .../framework/plugins/linux/pidhashtable.py | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 9a2528543c..3223aed4a3 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -44,17 +44,16 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ), ] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.vmlinux = None - self.vmlinux_layer = None - def _is_valid_task(self, task) -> bool: - return bool(task and task.pid > 0 and self.vmlinux_layer.is_valid(task.parent)) + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + return bool(task and task.pid > 0 and vmlinux_layer.is_valid(task.parent)) def _get_pidtype_pid(self): + vmlinux = self.context.modules[self.config["kernel"]] + # The pid_type enumeration is present since 2.5.37, just in case - pid_type_enum = self.vmlinux.get_enumeration("pid_type") + pid_type_enum = vmlinux.get_enumeration("pid_type") if not pid_type_enum: vollog.error("Cannot find pid_type enum. Unsupported kernel") return None @@ -68,38 +67,46 @@ def _get_pidtype_pid(self): return pidtype_pid def _get_pidhash_array(self): - pidhash_shift = self.vmlinux.object_from_symbol("pidhash_shift") + vmlinux = self.context.modules[self.config["kernel"]] + + pidhash_shift = vmlinux.object_from_symbol("pidhash_shift") pidhash_size = 1 << pidhash_shift - array_type_name = self.vmlinux.symbol_table_name + constants.BANG + "array" + array_type_name = vmlinux.symbol_table_name + constants.BANG + "array" - pidhash_ptr = self.vmlinux.object_from_symbol("pid_hash") + pidhash_ptr = vmlinux.object_from_symbol("pid_hash") # pidhash is an array of hlist_heads pidhash = self._context.object( array_type_name, offset=pidhash_ptr, - subtype=self.vmlinux.get_type("hlist_head"), + subtype=vmlinux.get_type("hlist_head"), count=pidhash_size, - layer_name=self.vmlinux.layer_name, + layer_name=vmlinux.layer_name, ) return pidhash def _walk_upid(self, seen_upids, upid): - while upid and self.vmlinux_layer.is_valid(upid.vol.offset): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + + while upid and vmlinux_layer.is_valid(upid.vol.offset): if upid.vol.offset in seen_upids: break seen_upids.add(upid.vol.offset) pid_chain = upid.pid_chain - if not (pid_chain and self.vmlinux_layer.is_valid(pid_chain.vol.offset)): + if not (pid_chain and vmlinux_layer.is_valid(pid_chain.vol.offset)): break upid = linux.LinuxUtilities.container_of( - pid_chain.next, "upid", "pid_chain", self.vmlinux + pid_chain.next, "upid", "pid_chain", vmlinux ) def _get_upids(self): + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + # 2.6.24 <= kernels < 4.15 pidhash = self._get_pidhash_array() @@ -108,10 +115,10 @@ def _get_upids(self): # each entry in the hlist is a upid which is wrapped in a pid ent = hlist.first - while ent and self.vmlinux_layer.is_valid(ent.vol.offset): + while ent and vmlinux_layer.is_valid(ent.vol.offset): # upid->pid_chain exists 2.6.24 <= kernel < 4.15 upid = linux.LinuxUtilities.container_of( - ent.vol.offset, "upid", "pid_chain", self.vmlinux + ent.vol.offset, "upid", "pid_chain", vmlinux ) if upid.vol.offset in seen_upids: @@ -124,16 +131,14 @@ def _get_upids(self): return seen_upids def _pid_hash_implementation(self): + vmlinux = self.context.modules[self.config["kernel"]] + # 2.6.24 <= kernels < 4.15 - task_pids_off = self.vmlinux.get_type("task_struct").relative_child_offset( - "pids" - ) + task_pids_off = vmlinux.get_type("task_struct").relative_child_offset("pids") pidtype_pid = self._get_pidtype_pid() for upid in self._get_upids(): - pid = linux.LinuxUtilities.container_of( - upid, "pid", "numbers", self.vmlinux - ) + pid = linux.LinuxUtilities.container_of(upid, "pid", "numbers", vmlinux) if not pid: continue @@ -141,22 +146,24 @@ def _pid_hash_implementation(self): if not pid_tasks_0: continue - task = self.vmlinux.object( + task = vmlinux.object( "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True ) if self._is_valid_task(task): yield task def _task_for_radix_pid_node(self, nodep): + vmlinux = self.context.modules[self.config["kernel"]] + # kernels >= 4.15 - pid = self.vmlinux.object("pid", offset=nodep, absolute=True) + pid = vmlinux.object("pid", offset=nodep, absolute=True) pidtype_pid = self._get_pidtype_pid() pid_tasks_0 = pid.tasks[pidtype_pid].first if not pid_tasks_0: return None - task_struct_type = self.vmlinux.get_type("task_struct") + task_struct_type = vmlinux.get_type("task_struct") if task_struct_type.has_member("pids"): member = "pids" elif task_struct_type.has_member("pid_links"): @@ -165,15 +172,17 @@ def _task_for_radix_pid_node(self, nodep): return None task_pids_off = task_struct_type.relative_child_offset(member) - task = self.vmlinux.object( + task = vmlinux.object( "task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True ) return task def _pid_namespace_idr(self): + vmlinux = self.context.modules[self.config["kernel"]] + # kernels >= 4.15 - ns_addr = self.vmlinux.get_symbol("init_pid_ns").address - ns = self.vmlinux.object("pid_namespace", offset=ns_addr) + ns_addr = vmlinux.get_symbol("init_pid_ns").address + ns = vmlinux.object("pid_namespace", offset=ns_addr) for page_addr in ns.idr.get_entries(): task = self._task_for_radix_pid_node(page_addr) @@ -181,24 +190,26 @@ def _pid_namespace_idr(self): yield task def _determine_pid_func(self): - pid_hash = self.vmlinux.has_symbol("pid_hash") and self.vmlinux.has_symbol( + vmlinux = self.context.modules[self.config["kernel"]] + + pid_hash = vmlinux.has_symbol("pid_hash") and vmlinux.has_symbol( "pidhash_shift" ) # 2.5.55 <= kernels < 4.15 - has_pid_numbers = self.vmlinux.has_type("pid") and self.vmlinux.get_type( + has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type( "pid" ).has_member( "numbers" ) # kernels >= 2.6.24 - has_pid_chain = self.vmlinux.has_type("upid") and self.vmlinux.get_type( + has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type( "upid" ).has_member( "pid_chain" ) # 2.6.24 <= kernels < 4.15 # kernels >= 4.15 - pid_idr = self.vmlinux.has_type("pid_namespace") and self.vmlinux.get_type( + pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type( "pid_namespace" ).has_member("idr") @@ -217,8 +228,6 @@ def get_tasks(self) -> interfaces.objects.ObjectInterface: Yields: task_struct objects """ - self.vmlinux = self.context.modules[self.config["kernel"]] - self.vmlinux_layer = self.context.layers[self.vmlinux.layer_name] pid_func = self._determine_pid_func() if not pid_func: vollog.error("Cannot determine which PID hash table this kernel is using") From d964e6f61de00b5a8608d48d468d9a59b7923ec7 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 02:06:06 +1000 Subject: [PATCH 071/250] PR review fixes: Check for LinuxUtilities version everywhere we use it --- .../symbols/linux/extensions/__init__.py | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d00af7a3f3..51dc37d315 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,6 +13,7 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion +from volatility3.framework.configuration import requirements from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1608,6 +1609,19 @@ def get_state(self): class bpf_prog(objects.StructType): + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def get_type(self) -> Union[str, None]: """Returns a string with the eBPF program type""" @@ -1631,7 +1645,7 @@ def get_tag(self) -> Union[str, None]: if not self.has_member("tag"): return None - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] prog_tag_addr = self.tag.vol.offset @@ -2043,13 +2057,26 @@ def get_flags_list(self) -> List[str]: return flags + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def to_paddr(self) -> int: """Converts a page's virtual address to its physical address using the current physical memory model. Returns: int: page physical address """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] vmemmap_start = None @@ -2100,7 +2127,7 @@ def get_content(self) -> Union[str, None]: Returns: The page content """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() @@ -2118,6 +2145,19 @@ class IDR(objects.StructType): MAX_IDR_SHIFT = INT_SIZE * 8 - 1 MAX_IDR_BIT = 1 << MAX_IDR_SHIFT + def _get_vmlinux(self): + linuxutils_required_version = (2, 1, 1) + linuxutils_current_version = linux.LinuxUtilities._version + if not requirements.VersionRequirement.matches_required( + linuxutils_required_version, linuxutils_current_version + ): + raise exceptions.PluginRequirementException( + f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" + ) + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return vmlinux + def idr_max(self, num_layers: int) -> int: """Returns the maximum ID which can be allocated given idr::layers @@ -2141,7 +2181,7 @@ def idr_find(self, idr_id: int) -> int: Returns: A pointer to the given ID element """ - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + vmlinux = self._get_vmlinux() if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" From d627f243e259efce2f730860f47d9116b3c78f3d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 10:55:01 +1000 Subject: [PATCH 072/250] PR review fixes: Add typing info to the get_inodes() class method. --- volatility3/framework/plugins/linux/pagecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index cf4151c852..e384cbabfd 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -6,7 +6,7 @@ import logging import datetime from dataclasses import dataclass, astuple -from typing import List, Set, Type +from typing import List, Set, Type, Iterable from volatility3.framework import renderers, interfaces from volatility3.framework.renderers import format_hints @@ -205,7 +205,7 @@ def get_inodes( cls, context: interfaces.context.ContextInterface, config_path: str, - ): + ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: From 90b327e63253404a98cc5a79e3cecaa1b773048c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 11:45:29 +1000 Subject: [PATCH 073/250] PR review fixes: Make mountinfo.get_superblocks() a classmethod and adapt the code using it. --- .../framework/plugins/linux/mountinfo.py | 19 +++++++++--- .../framework/plugins/linux/pagecache.py | 31 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index dfb2e2f523..1eaec77bf0 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface): _required_framework_version = (2, 2, 0) - _version = (1, 1, 0) + _version = (1, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -143,8 +143,8 @@ def get_mountinfo( sb_opts, ) + @staticmethod def _get_tasks_mountpoints( - self, tasks: Iterable[interfaces.objects.ObjectInterface], filtered_by_pids: bool = False, ): @@ -247,17 +247,26 @@ def _generator( "Could not filter by mount namespace id. This field is not available in this kernel." ) - def get_superblocks(self): + @classmethod + def get_superblocks( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Yield file system superblocks based on the task's mounted filesystems. + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Yields: super_block: Kernel's struct super_block object """ # No filter so that we get all the mount namespaces from all tasks - tasks = pslist.PsList.list_tasks(self.context, self.config["kernel"]) + tasks = pslist.PsList.list_tasks(context, vmlinux_module_name) seen_sb_ptr = set() - for task, mnt, _mnt_ns_id in self._get_tasks_mountpoints(tasks): + for task, mnt, _mnt_ns_id in cls._get_tasks_mountpoints(tasks): path_root = linux.LinuxUtilities.get_path_mnt(task, mnt) if not path_root: continue diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index e384cbabfd..e54891480f 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -115,7 +115,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( - name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 1, 0) + name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0) ), requirements.ListRequirement( name="type", @@ -204,22 +204,22 @@ def _walk_dentry( def get_inodes( cls, context: interfaces.context.ContextInterface, - config_path: str, + vmlinux_module_name: str, ) -> Iterable[InodeInternal]: """Retrieves the inodes from the superblocks Args: context: The context that the plugin will operate within - config_path: The path to configuration data within the context configuration data + vmlinux_module_name: The name of the kernel module on which to operate Yields: An InodeInternal object """ - superblocks_iter = mountinfo.MountInfo( + superblocks_iter = mountinfo.MountInfo.get_superblocks( context=context, - config_path=config_path, - ).get_superblocks() + vmlinux_module_name=vmlinux_module_name, + ) seen_inodes = set() seen_dentries = set() @@ -289,11 +289,13 @@ def get_inodes( yield inode_in def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] inodes_iter = self.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) types_filter = self.config["type"] @@ -316,12 +318,15 @@ def generate_timeline(self): These need not be generated in any particular order, sorting will be done later """ - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] inodes_iter = self.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) + for inode_in in inodes_iter: inode_out = inode_in.to_user(vmlinux_layer) description = f"Cached Inode for {inode_out.path}" @@ -450,7 +455,8 @@ def write_inode_content_to_file( vollog.error("Unable to write to file (%s): %s", filename, e) def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] vmlinux_layer = self.context.layers[vmlinux.layer_name] if self.config["inode"] and self.config["find"]: @@ -459,7 +465,8 @@ def _generator(self): if self.config["find"]: inodes_iter = Files.get_inodes( - context=self.context, config_path=self.config_path + context=self.context, + vmlinux_module_name=vmlinux_module_name, ) for inode_in in inodes_iter: if inode_in.path == self.config["find"]: From 3bf9f8cec0e1c4c75088abe17baadd0bc6d4c3d6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 24 Aug 2024 12:03:10 +1000 Subject: [PATCH 074/250] PR review fixes: Add typing info to pagecache.Files._follow_symlink() --- volatility3/framework/plugins/linux/pagecache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index e54891480f..b6c5f7cc08 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -131,7 +131,10 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] @staticmethod - def _follow_symlink(inode, symlink_path) -> str: + def _follow_symlink( + inode: interfaces.objects.ObjectInterface, + symlink_path: str, + ) -> str: """Follows (fast) symlinks (kernels >= 4.2.x). Fast symlinks are filesystem agnostic. From e97abae156721bc1c625729166c08325c164bd3a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 24 Aug 2024 15:02:31 +0100 Subject: [PATCH 075/250] Remove get_vmlinux calls Sorry, I know I asked for it, but I hadn't quite figured out what was going on. Things in the symbols/linux code are considered part of the framework, and therefore it's the framework version that should have been bumped. Since the framework comes packages with LinuxUtilities we can rely on the version numbers to be suitable. This cleans up the mess I caused, sorry for the extra work! 5:S --- volatility3/framework/constants/_version.py | 4 +- .../symbols/linux/extensions/__init__.py | 47 ++----------------- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 4df0b90414..d30446d63b 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,7 +1,7 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 8 # Number of changes that only add to the interface -VERSION_PATCH = 1 # Number of changes that do not change the interface +VERSION_MINOR = 9 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" PACKAGE_VERSION = ( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 51dc37d315..fdd34403ac 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1609,19 +1609,6 @@ def get_state(self): class bpf_prog(objects.StructType): - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def get_type(self) -> Union[str, None]: """Returns a string with the eBPF program type""" @@ -1645,7 +1632,7 @@ def get_tag(self) -> Union[str, None]: if not self.has_member("tag"): return None - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] prog_tag_addr = self.tag.vol.offset @@ -2057,26 +2044,13 @@ def get_flags_list(self) -> List[str]: return flags - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def to_paddr(self) -> int: """Converts a page's virtual address to its physical address using the current physical memory model. Returns: int: page physical address """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] vmemmap_start = None @@ -2127,7 +2101,7 @@ def get_content(self) -> Union[str, None]: Returns: The page content """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name] physical_layer = vmlinux.context.layers["memory_layer"] page_paddr = self.to_paddr() @@ -2145,19 +2119,6 @@ class IDR(objects.StructType): MAX_IDR_SHIFT = INT_SIZE * 8 - 1 MAX_IDR_BIT = 1 << MAX_IDR_SHIFT - def _get_vmlinux(self): - linuxutils_required_version = (2, 1, 1) - linuxutils_current_version = linux.LinuxUtilities._version - if not requirements.VersionRequirement.matches_required( - linuxutils_required_version, linuxutils_current_version - ): - raise exceptions.PluginRequirementException( - f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}" - ) - - vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) - return vmlinux - def idr_max(self, num_layers: int) -> int: """Returns the maximum ID which can be allocated given idr::layers @@ -2181,7 +2142,7 @@ def idr_find(self, idr_id: int) -> int: Returns: A pointer to the given ID element """ - vmlinux = self._get_vmlinux() + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" From 6a157a785f3364635de2c7a43eeab054ab7c6b3d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 27 Aug 2024 14:45:52 +0100 Subject: [PATCH 076/250] Bump the modules version number Pull-request #1173 bumped the version of the modules plugin (even though this only needed to be a MINOR version bump, see https://github.com/volatilityfoundation/volatility3/pull/1173#discussion_r1649614761), but failed to verify that other plugins which relied on it were also updated to make use of the new plugin. This was the version system working as intended, but highlighted a review failure that the neither the author, nor the reviewers, verified that the rest of the framework (specifically other plugins which relied on modules) worked correctly with the new code (which this kind of error is designed to fix). Fixes #1244. --- volatility3/framework/plugins/windows/ssdt.py | 2 +- volatility3/framework/plugins/windows/verinfo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/ssdt.py b/volatility3/framework/plugins/windows/ssdt.py index 6a47c36e9f..1fcb6cc910 100644 --- a/volatility3/framework/plugins/windows/ssdt.py +++ b/volatility3/framework/plugins/windows/ssdt.py @@ -30,7 +30,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] architectures=["Intel32", "Intel64"], ), requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(1, 0, 0) + name="modules", plugin=modules.Modules, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 1c6615804a..5b3c52bf6a 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -46,7 +46,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), requirements.PluginRequirement( - name="modules", plugin=modules.Modules, version=(1, 0, 0) + name="modules", plugin=modules.Modules, version=(2, 0, 0) ), requirements.VersionRequirement( name="dlllist", component=dlllist.DllList, version=(2, 0, 0) From a2196850bc01d5a1a41a2aa29685cdb1d2146e87 Mon Sep 17 00:00:00 2001 From: Davide Arcuri Date: Wed, 28 Aug 2024 10:39:28 +0200 Subject: [PATCH 077/250] increase python version to 3.8 --- .github/workflows/build-pypi.yml | 2 +- .github/workflows/install.yml | 2 +- .github/workflows/test.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-pypi.yml b/.github/workflows/build-pypi.yml index 346d0337e9..d1a63b4da7 100644 --- a/.github/workflows/build-pypi.yml +++ b/.github/workflows/build-pypi.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7"] + python-version: ["3.8"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index e13161085c..398ff8ae36 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: host: [ ubuntu-latest, windows-latest ] - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + python-version: [ "3.8", "3.9", "3.10", "3.11" ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 55b2e4b602..65dfb13c84 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7"] + python-version: ["3.8"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} From 7a479d617f8107590533eaed9afe105c3cd3b34f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 28 Aug 2024 18:40:41 +0200 Subject: [PATCH 078/250] add OS and framework architectures constants --- .../framework/constants/architectures.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 volatility3/framework/constants/architectures.py diff --git a/volatility3/framework/constants/architectures.py b/volatility3/framework/constants/architectures.py new file mode 100644 index 0000000000..a8822d7d32 --- /dev/null +++ b/volatility3/framework/constants/architectures.py @@ -0,0 +1,21 @@ +from volatility3.framework.layers import intel + +WIN_ARCHS = ["Intel32", "Intel64"] +"""Windows supported architectures""" +WIN_ARCHS_LAYERS = [intel.Intel] +"""Windows supported architectures layers""" + +LINUX_ARCHS = ["Intel32", "Intel64"] +"""Linux supported architectures""" +LINUX_ARCHS_LAYERS = [intel.Intel] +"""Linux supported architectures layers""" + +MAC_ARCHS = ["Intel32", "Intel64"] +"""Mac supported architectures""" +MAC_ARCHS_LAYERS = [intel.Intel] +"""Mac supported architectures layers""" + +FRAMEWORK_ARCHS = ["Intel32", "Intel64"] +"""Framework supported architectures""" +FRAMEWORK_ARCHS_LAYERS = [intel.Intel] +"""Framework supported architectures layers""" From 46a26c7dc5f5bc87d93027a6c005acbf85bc8716 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 2 Sep 2024 12:17:16 -0500 Subject: [PATCH 079/250] Address feedback --- .../framework/plugins/windows/orphan_kernel_threads.py | 8 +++++--- volatility3/framework/plugins/windows/thrdscan.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index 1cdb1dcdfa..f4901dc8c9 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -1,4 +1,4 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # @@ -48,8 +48,9 @@ def list_orphan_kernel_threads( """Yields thread objects of kernel threads that do not map to a module Args: - kernel - + cls + context: the context to operate upon + module_name: name of the module to use for scanning Returns: A generator of thread objects of orphaned threads """ @@ -61,6 +62,7 @@ def list_orphan_kernel_threads( context, layer_name, symbol_table ) + # FIXME - use a proper constant once established # used to filter out smeared pointers if symbols.symbol_table_is_64bit(context, symbol_table): kernel_start = 0xFFFFF80000000000 diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index ad885e8e9c..b812a15ffb 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -48,8 +48,7 @@ def scan_threads( Args: context: The context to retrieve required elements (layers, symbol tables) from - layer_name: The name of the layer on which to operate - symbol_table: The name of the table containing the kernel symbols + module_name: Name of the module to use for scanning Returns: A list of _ETHREAD objects found by scanning memory for the "Thre" / "Thr\\xE5" pool signatures From 483cb7e4eebbb6b536cdec883caa3f75fa182ce2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 2 Sep 2024 15:08:29 -0500 Subject: [PATCH 080/250] Add smear checks in MFT parsing code --- .../framework/symbols/windows/extensions/mft.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index 14c1f08d63..c51dd03487 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -27,6 +27,11 @@ class MFTAttribute(objects.StructType): """This represents an MFT ATTRIBUTE""" def get_resident_filename(self) -> str: + # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems + # Length as 512 as its 256*2, which is the maximum size for an entire file path, so this is even generous + if self.Attr_Header.ContentOffset > 4194304 or self.Attr_Header.NameLength > 512: + return None + # To get the resident name, we jump to relative name offset and read name length * 2 bytes of data try: name = self._context.object( @@ -42,6 +47,11 @@ def get_resident_filename(self) -> str: return None def get_resident_filecontent(self) -> bytes: + # smear observed in mass testing of samples + # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems + if self.Attr_Header.ContentOffset > 4194304 or self.Attr_Header.ContentLength > 4194304: + return None + # To get the resident content, we jump to relative content offset and read name length * 2 bytes of data try: bytesobj = self._context.object( From 660e8a7b89387bfc3e883a10a3662708a09fa146 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Mon, 2 Sep 2024 15:09:20 -0500 Subject: [PATCH 081/250] Add smear checks in MFT parsing code --- .../framework/symbols/windows/extensions/mft.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index c51dd03487..c0303eafb1 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -29,7 +29,10 @@ class MFTAttribute(objects.StructType): def get_resident_filename(self) -> str: # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems # Length as 512 as its 256*2, which is the maximum size for an entire file path, so this is even generous - if self.Attr_Header.ContentOffset > 4194304 or self.Attr_Header.NameLength > 512: + if ( + self.Attr_Header.ContentOffset > 4194304 + or self.Attr_Header.NameLength > 512 + ): return None # To get the resident name, we jump to relative name offset and read name length * 2 bytes of data @@ -49,7 +52,10 @@ def get_resident_filename(self) -> str: def get_resident_filecontent(self) -> bytes: # smear observed in mass testing of samples # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems - if self.Attr_Header.ContentOffset > 4194304 or self.Attr_Header.ContentLength > 4194304: + if ( + self.Attr_Header.ContentOffset > 4194304 + or self.Attr_Header.ContentLength > 4194304 + ): return None # To get the resident content, we jump to relative content offset and read name length * 2 bytes of data From 9c058936a88893d0b5dd6a3ee68ab87b037bacf4 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Tue, 3 Sep 2024 10:30:05 -0500 Subject: [PATCH 082/250] Address feedback --- .../framework/symbols/windows/extensions/mft.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index c0303eafb1..ebba882c0b 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -2,6 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +from typing import Optional + from volatility3.framework import objects, constants, exceptions @@ -26,11 +28,11 @@ def get_full_name(self) -> str: class MFTAttribute(objects.StructType): """This represents an MFT ATTRIBUTE""" - def get_resident_filename(self) -> str: + def get_resident_filename(self) -> Optional[str]: # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems # Length as 512 as its 256*2, which is the maximum size for an entire file path, so this is even generous if ( - self.Attr_Header.ContentOffset > 4194304 + self.Attr_Header.ContentOffset > 0x400000 or self.Attr_Header.NameLength > 512 ): return None @@ -49,12 +51,12 @@ def get_resident_filename(self) -> str: except exceptions.InvalidAddressException: return None - def get_resident_filecontent(self) -> bytes: + def get_resident_filecontent(self) -> Optional[bytes]: # smear observed in mass testing of samples # 4MB chosen as cutoff instead of 4KB to allow for recovery from format /L created file systems if ( - self.Attr_Header.ContentOffset > 4194304 - or self.Attr_Header.ContentLength > 4194304 + self.Attr_Header.ContentOffset > 0x400000 + or self.Attr_Header.ContentLength > 0x400000 ): return None From 69e6e59daf39ec7a0cf9a82fb13762d4389c95a4 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:01:21 -0500 Subject: [PATCH 083/250] Add new pe_symbols API, debug registers plugin, unhooked system calls plugin --- .../plugins/windows/debugregisters.py | 223 ++++++ .../framework/plugins/windows/pe_symbols.py | 732 ++++++++++++++++++ .../plugins/windows/unhooked_system_calls.py | 183 +++++ .../framework/plugins/windows/vadinfo.py | 88 ++- 4 files changed, 1219 insertions(+), 7 deletions(-) create mode 100644 volatility3/framework/plugins/windows/debugregisters.py create mode 100644 volatility3/framework/plugins/windows/pe_symbols.py create mode 100644 volatility3/framework/plugins/windows/unhooked_system_calls.py diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py new file mode 100644 index 0000000000..5f3c2e5194 --- /dev/null +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -0,0 +1,223 @@ +import logging + +from typing import Tuple, Optional, Generator, List, Dict + +from functools import partial + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +import volatility3.plugins.windows.pslist as pslist +import volatility3.plugins.windows.threads as threads +import volatility3.plugins.windows.vadinfo as vadinfo +import volatility3.plugins.windows.pe_symbols as pe_symbols + +vollog = logging.getLogger(__name__) + + +class DebugRegisters(interfaces.plugins.PluginInterface): + # version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags + _required_framework_version = (2, 6, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0) + ), + ] + + def _get_debug_info( + self, ethread: interfaces.objects.ObjectInterface + ) -> Optional[Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]]: + """ + Gathers information related to the debug registers for the given thread + """ + try: + dr7 = ethread.Tcb.TrapFrame.Dr7 + state = ethread.Tcb.State + except exceptions.InvalidAddressException: + return None + + # 0 = debug registers not active + # 4 = terminated + if dr7 == 0 or state == 4: + return None + + try: + owner_proc = ethread.owning_process() + except (AttributeError, exceptions.InvalidAddressException): + return None + + dr0 = ethread.Tcb.TrapFrame.Dr0 + dr1 = ethread.Tcb.TrapFrame.Dr1 + dr2 = ethread.Tcb.TrapFrame.Dr2 + dr3 = ethread.Tcb.TrapFrame.Dr3 + + # bail if all are 0 + if not (dr0 or dr1 or dr2 or dr3): + return None + + return owner_proc, dr7, dr0, dr1, dr2, dr3 + + def _get_vads( + self, + vads_cache: Dict[int, List[Tuple[int, int, str]]], + owner_proc: interfaces.objects.ObjectInterface, + ) -> Optional[List[Tuple[int, int, str]]]: + if owner_proc.vol.offset in vads_cache: + vads = vads_cache[owner_proc.vol.offset] + else: + vads = vadinfo.VadInfo.get_proc_vads_with_file_paths(owner_proc) + vads_cache[owner_proc.vol.offset] = vads + + # smear or terminated process + if len(vads) == 0: + return None + + return vads + + def _generator( + self, + ) -> Generator[ + Tuple[ + int, + Tuple[ + str, + int, + int, + int, + int, + format_hints.Hex, + str, + str, + format_hints.Hex, + str, + str, + format_hints.Hex, + str, + str, + format_hints.Hex, + str, + str, + ], + ], + None, + None, + ]: + kernel = self.context.modules[self.config["kernel"]] + + vads_cache: Dict[int, List[Tuple[int, int, str]]] = {} + + proc_modules = None + + procs = pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) + + for proc in procs: + for thread in threads.Threads.list_threads(kernel, proc): + debug_info = self._get_debug_info(thread) + if not debug_info: + continue + + owner_proc, dr7, dr0, dr1, dr2, dr3 = debug_info + + vads = self._get_vads(vads_cache, owner_proc) + if not vads: + continue + + # this lookup takes a while, so only perform if we need to + if not proc_modules: + proc_modules = pe_symbols.PESymbols.get_process_modules( + self.context, kernel.layer_name, kernel.symbol_table_name, None + ) + path_and_symbol = partial( + pe_symbols.PESymbols.path_and_symbol_for_address, + self.context, + self.config_path, + proc_modules, + ) + + file0, sym0 = path_and_symbol(vads, dr0) + file1, sym1 = path_and_symbol(vads, dr1) + file2, sym2 = path_and_symbol(vads, dr2) + file3, sym3 = path_and_symbol(vads, dr3) + + # if none map to an actual file VAD then bail + if not ( + isinstance(file0, str) + or isinstance(file1, str) + or isinstance(file2, str) + or isinstance(file3, str) + ): + continue + + process_name = owner_proc.ImageFileName.cast( + "string", + max_length=owner_proc.ImageFileName.vol.count, + errors="replace", + ) + + thread_tid = thread.Cid.UniqueThread + + yield ( + 0, + ( + process_name, + owner_proc.UniqueProcessId, + thread_tid, + thread.Tcb.State, + dr7, + format_hints.Hex(dr0), + file0, + sym0, + format_hints.Hex(dr1), + file1, + sym1, + format_hints.Hex(dr2), + file2, + sym2, + format_hints.Hex(dr3), + file3, + sym3, + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("TID", int), + ("State", int), + ("Dr7", int), + ("Dr0", format_hints.Hex), + ("Range0", str), + ("Symbol0", str), + ("Dr1", format_hints.Hex), + ("Range1", str), + ("Symbol1", str), + ("Dr2", format_hints.Hex), + ("Range2", str), + ("Symbol2", str), + ("Dr3", format_hints.Hex), + ("Range3", str), + ("Symbol3", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py new file mode 100644 index 0000000000..b7faeaf555 --- /dev/null +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -0,0 +1,732 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 + + +import io +import logging + +from typing import Dict, Tuple, Optional, List, Generator, Union + +import pefile + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows import pdbutil +from volatility3.framework.symbols.windows.extensions import pe +from volatility3.plugins.windows import pslist, vadinfo, modules + +vollog = logging.getLogger(__name__) + + +class PESymbolFinder: + """ + Interface for PE symbol finding classes + This interface provides a standard way for the calling code to + lookup symbols by name or address + """ + + cached_str = Union[str, None] + cached_str_dict = Dict[str, cached_str] + + cached_int = Union[int, None] + cached_int_dict = Dict[str, cached_int] + + cached_value = Union[int, str, None] + cached_value_dict = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] + + def __init__( + self, + layer_name: str, + mod_name: str, + module_start: int, + symbol_module: Union[interfaces.context.ModuleInterface, pefile.ExportDirData], + ): + self._layer_name = layer_name + self._mod_name = mod_name + self._module_start = module_start + self._symbol_module = symbol_module + + self._address_cache: PESymbolFinder.cached_int_dict = {} + self._name_cache: PESymbolFinder.cached_str_dict = {} + + def _get_cache_key(self, value: cached_value) -> str: + """ + Maintain a cache for symbol lookups to avoid re-walking of PDB symbols or export tables + within the same module for the same address in the same layer + """ + return f"{self._layer_name}|{self._mod_name}|{value}" + + def get_name_for_address(self, address: int) -> cached_str: + cached_key = self._get_cache_key(address) + if cached_key not in self._name_cache: + name = self._do_get_name(address) + self._name_cache[cached_key] = name + + return self._name_cache[cached_key] + + def get_address_for_name(self, name: str) -> cached_int: + cached_key = self._get_cache_key(name) + if cached_key not in self._address_cache: + address = self._do_get_address(name) + self._address_cache[cached_key] = address + + return self._address_cache[cached_key] + + def _do_get_name(self, address: int) -> cached_str: + raise NotImplementedError("_do_get_name must be overwritten") + + def _do_get_address(self, name: str) -> cached_int: + raise NotImplementedError("_do_get_address must be overwritten") + + +class PDBSymbolFinder(PESymbolFinder): + """ + PESymbolFinder implementation for PDB modules + """ + + def _do_get_address(self, name: str) -> PESymbolFinder.cached_int: + try: + return self._symbol_module.get_absolute_symbol_address(name) + except exceptions.SymbolError: + return None + + def _do_get_name(self, address: int) -> PESymbolFinder.cached_str: + try: + name = self._symbol_module.get_symbols_by_absolute_location(address)[0] + return name.split(constants.BANG)[1] + except (exceptions.SymbolError, IndexError): + return None + + +class ExportSymbolFinder(PESymbolFinder): + """ + PESymbolFinder implementation for PDB modules + """ + + def _get_name(self, export: pefile.ExportData) -> Optional[str]: + # AttributeError throws on empty or ordinal-only exports + try: + return export.name.decode("ascii") + except AttributeError: + return None + + def _do_get_name(self, address: int) -> PESymbolFinder.cached_str: + for export in self._symbol_module: + if export.address + self._module_start == address: + return self._get_name(export) + + return None + + def _do_get_address(self, name: str) -> PESymbolFinder.cached_int: + for export in self._symbol_module: + sym_name = self._get_name(export) + if sym_name and sym_name == name: + return self._module_start + export.address + + return None + + +class PESymbols(interfaces.plugins.PluginInterface): + """Prints symbols in PE files in process and kernel memory""" + + _required_framework_version = (2, 7, 0) + + _version = (1, 0, 0) + + # used for special handling of the kernel PDB file. See later notes + os_module_name = "ntoskrnl.exe" + + # keys for specifying wanted names and/or addresses + # used for consistent access between the API and plugins + wanted_names = "names" + wanted_addresses = "addresses" + + # how wanted modules/symbols are specified, such as: + # {"ntdll.dll" : {wanted_addresses : [42, 43, 43]}} + # {"ntdll.dll" : {wanted_names : ["NtCreateThread"]}} + filter_modules_type = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] + + # holds resolved symbols + # {"ntdll.dll": [("Bob", 123), ("Alice", 456)]} + found_symbols_type = Dict[str, List[Tuple[str, int]]] + + # used to hold informatin about a range (VAD or kernel module) + # (start address, size, file path) + range_type = Tuple[int, int, str] + ranges_type = List[range_type] + + @classmethod + def get_requirements(cls) -> List: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), + requirements.ChoiceRequirement( + name="source", + description="Where to resolve symbols.", + choices=["kernel", "processes"], + optional=False, + ), + requirements.StringRequirement( + name="module", + description='Module in which to resolve symbols. Use "ntoskrnl.exe" to resolve in the base kernel executable.', + optional=False, + ), + requirements.StringRequirement( + name="symbol", + description="Symbol name to resolve", + optional=True, + ), + requirements.IntRequirement( + name="address", + description="Address of symbol to resolve", + optional=True, + ), + ] + + @staticmethod + def _get_pefile_obj( + context: interfaces.context.ContextInterface, + pe_table_name: str, + layer_name: str, + base_address: int, + ) -> Optional[pefile.PE]: + """ + Attempts to pefile object from the bytes of the PE file + + Args: + pe_table_name: name of the pe types table + layer_name: name of the process layer + base_address: base address of the module + + Returns: + the constructed pefile object + """ + pe_data = io.BytesIO() + + try: + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=base_address, + layer_name=layer_name, + ) + + for offset, data in dos_header.reconstruct(): + pe_data.seek(offset) + pe_data.write(data) + + pe_ret = pefile.PE(data=pe_data.getvalue(), fast_load=True) + + except exceptions.InvalidAddressException: + pe_ret = None + + return pe_ret + + @staticmethod + def range_info_for_address( + ranges: ranges_type, address: int + ) -> Optional[range_type]: + """ + Helper for getting the range information for an address + """ + for start, size, filepath in ranges: + if start <= address < start + size: + return start, size, filepath + + return None + + @staticmethod + def filepath_for_address(ranges: ranges_type, address: int) -> Optional[str]: + """ + Helper to get the file path for an address + """ + info = PESymbols.range_info_for_address(ranges, address) + if info: + return info[2] + + return None + + @staticmethod + def filename_for_path(filepath: str) -> str: + """ + Consistent way to get the filename + """ + return filepath.split("\\")[-1] + + @staticmethod + def addresses_for_process_symbols( + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + symbol_table_name: str, + symbols: filter_modules_type, + ) -> found_symbols_type: + collected_modules = PESymbols.get_process_modules( + context, layer_name, symbol_table_name, symbols + ) + + found_symbols = PESymbols.find_symbols( + context, config_path, symbols, collected_modules + ) + + for mod_name, unresolved_symbols in symbols.items(): + for symbol in unresolved_symbols: + vollog.debug(f"Unable to resolve symbol {symbol} in module {mod_name}") + + return found_symbols + + @staticmethod + def path_and_symbol_for_address( + context: interfaces.context.ContextInterface, + config_path: str, + collected_modules: Dict[str, List[Tuple[str, int, int]]], + ranges: ranges_type, + address: int, + ) -> Tuple[str, str]: + """ + Method for plugins to determine the file path and symbol name for a given address + + collected_modules: return value from `get_kernel_modules` or `get_process_modules` + ranges: the memory ranges to examine in this layer. + address: address to resolve to its symbol name + """ + + if not address: + return renderers.NotApplicableValue(), renderers.NotApplicableValue() + + filepath = PESymbols.filepath_for_address(ranges, address) + + if not filepath: + return renderers.NotAvailableValue(), renderers.NotAvailableValue() + + filename = PESymbols.filename_for_path(filepath).lower() + + # setup to resolve the address + filter_module: PESymbols.filter_modules_type = { + filename: {PESymbols.wanted_addresses: [address]} + } + + found_symbols = PESymbols.find_symbols( + context, config_path, filter_module, collected_modules + ) + + if not found_symbols or not found_symbols[filename]: + return renderers.NotAvailableValue(), renderers.NotAvailableValue() + + return filepath, found_symbols[filename][0][0] + + @staticmethod + def _get_exported_symbols( + context: interfaces.context.ContextInterface, + pe_table_name: str, + mod_name: str, + module_info: Tuple[str, int, int], + ) -> Optional[ExportSymbolFinder]: + """ + Attempts to locate symbols based on export analysis + + mod_name: lower case name of the module to resolve symbols in + module_info: (layer_name, module_start, module_size) of the module to examine + """ + + layer_name = module_info[0] + module_start = module_info[1] + + # we need a valid PE with an export table + pe_module = PESymbols._get_pefile_obj( + context, pe_table_name, layer_name, module_start + ) + if not pe_module: + return None + + pe_module.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + if not hasattr(pe_module, "DIRECTORY_ENTRY_EXPORT"): + return None + + return ExportSymbolFinder( + layer_name, mod_name, module_start, pe_module.DIRECTORY_ENTRY_EXPORT.symbols + ) + + @staticmethod + def _get_pdb_module( + context: interfaces.context.ContextInterface, + config_path: str, + mod_name: str, + module_info: Tuple[str, int, int], + ) -> Optional[PDBSymbolFinder]: + """ + Attempts to locate symbols based on PDB analysis + + mod_name: lower case name of the module to resolve symbols in + module_info: (layer_name, module_start, module_size) of the module to examine + """ + + mod_symbols = None + + layer_name, module_start, module_size = module_info + + # the PDB name of the kernel file is not consistent for an exe, for example, + # a `ntoskrnl.exe` can have an internal PDB name of any of the ones in the following list + # The code attempts to find all possible PDBs to ensure the best chance of recovery + if mod_name == PESymbols.os_module_name: + pdb_names = ["ntkrnlmp.pdb", "ntkrnlpa.pdb", "ntkrpamp.pdb", "ntoskrnl.pdb"] + + # for non-kernel files, replace the exe, sys, or dll extension with pdb + else: + mod_name = mod_name[:-3] + "pdb" + first_upper = mod_name[0].upper() + mod_name[1:] + pdb_names = [mod_name, first_upper] + + # loop through each PDB name (will be just one for all but the kernel) + for pdb_name in pdb_names: + try: + mod_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( + context, + interfaces.configuration.path_join(config_path, mod_name), + layer_name, + pdb_name, + module_start, + module_size, + ) + + if mod_symbols: + break + + # this exception is expected when the PDB can't be found or downloaded + except exceptions.VolatilityException: + continue + + # this is not expected - it means pdbconv broke when parsing the PDB + except TypeError as e: + vollog.error( + f"Unable to parse PDB file for module {pdb_name} -> {e}. Please file a bug on the GitHub issue tracker." + ) + + # cannot do anything without the symbols + if not mod_symbols: + return None + + pdb_module = context.module( + mod_symbols, layer_name=layer_name, offset=module_start + ) + + return PDBSymbolFinder(layer_name, mod_name, module_start, pdb_module) + + @staticmethod + def _find_symbols_through_pdb( + context: interfaces.context.ContextInterface, + config_path: str, + module_instances: List[Tuple[str, int, int]], + mod_name: str, + ) -> Generator[PDBSymbolFinder, None, None]: + """ + Attempts to resolve the symbols in `wanted_symbols` through PDB analysis + """ + for module_info in module_instances: + mod_module = PESymbols._get_pdb_module( + context, config_path, mod_name, module_info + ) + if mod_module: + yield mod_module + + @staticmethod + def _find_symbols_through_exports( + context: interfaces.context.ContextInterface, + config_path: str, + module_instances: List[Tuple[str, int, int]], + mod_name: str, + ) -> Generator[ExportSymbolFinder, None, None]: + """ + Attempts to resolve the symbols in `wanted_symbols` through export analysis + """ + pe_table_name = intermed.IntermediateSymbolTable.create( + context, config_path, "windows", "pe", class_types=pe.class_types + ) + + # for each process layer and VAD, construct a PE and examine the export table + for module_info in module_instances: + exported_symbols = PESymbols._get_exported_symbols( + context, pe_table_name, mod_name, module_info + ) + if exported_symbols: + yield exported_symbols + + @staticmethod + def _get_symbol_value( + wanted_modules: PESymbolFinder.cached_value_dict, + mod_name: str, + symbol_resolver: PESymbolFinder, + ) -> Generator[Tuple[str, int], None, None]: + """ + Enumerates the symbols specified as wanted by the calling plugin + """ + wanted_symbols = wanted_modules[mod_name] + + if ( + PESymbols.wanted_names not in wanted_symbols + and PESymbols.wanted_addresses not in wanted_symbols + ): + vollog.warning( + f"Invalid `wanted_symbols` sent to `find_symbols` for module {mod_name}. addresses and names keys both misssing." + ) + return + + symbol_keys = [ + (PESymbols.wanted_names, "get_address_for_name"), + (PESymbols.wanted_addresses, "get_name_for_address"), + ] + + for symbol_key, symbol_getter in symbol_keys: + # address or name + if symbol_key in wanted_symbols: + # walk each wanted address or name + for wanted_value in wanted_symbols[symbol_key]: + symbol_value = symbol_resolver.__getattribute__(symbol_getter)( + wanted_value + ) + if symbol_value: + # yield out symbol name, symbol address + if symbol_key == PESymbols.wanted_names: + yield wanted_value, symbol_value # type: ignore + else: + yield symbol_value, wanted_value # type: ignore + + index = wanted_modules[mod_name][symbol_key].index(wanted_value) # type: ignore + + del wanted_modules[mod_name][symbol_key][index] + + # if all names or addresses from a module are found, delete the key + if not wanted_modules[mod_name][symbol_key]: + del wanted_modules[mod_name][symbol_key] + break + + @staticmethod + def _resolve_symbols_through_methods( + context: interfaces.context.ContextInterface, + config_path: str, + module_instances: List[Tuple[str, int, int]], + wanted_modules: PESymbolFinder.cached_value_dict, + mod_name: str, + ) -> Generator[Tuple[str, int], None, None]: + """ + Attempts to resolve every wanted symbol in `mod_name` + Every layer is enumerated for maximum chance of recovery + """ + symbol_resolving_methods = [ + PESymbols._find_symbols_through_pdb, + PESymbols._find_symbols_through_exports, + ] + + for method in symbol_resolving_methods: + for symbol_resolver in method( + context, config_path, module_instances, mod_name + ): + vollog.debug(f"Have resolver for method {method}") + yield from PESymbols._get_symbol_value( + wanted_modules, mod_name, symbol_resolver + ) + + if not wanted_modules[mod_name]: + break + + if not wanted_modules[mod_name]: + break + + @staticmethod + def find_symbols( + context: interfaces.context.ContextInterface, + config_path: str, + wanted_modules: PESymbolFinder.cached_value_dict, + collected_modules: Dict[str, List[Tuple[str, int, int]]], + ) -> found_symbols_type: + """ + Loops through each method of symbol analysis until each wanted symbol is found + Returns the resolved symbols as a dictionary that includes the name and runtime address + """ + found_symbols: PESymbols.found_symbols_type = {} + + for mod_name in wanted_modules: + if mod_name not in collected_modules: + continue + + module_instances = collected_modules[mod_name] + + # try to resolve the symbols for `mod_name` through each method (PDB and export table currently) + for symbol_name, address in PESymbols._resolve_symbols_through_methods( + context, config_path, module_instances, wanted_modules, mod_name + ): + if mod_name not in found_symbols: + found_symbols[mod_name] = [] + + found_symbols[mod_name].append((symbol_name, address)) + + # stop processing the layers (processes) if we found all the symbols for this module + if not wanted_modules[mod_name]: + break + + # stop processing this module if/when all symbols are found + if not wanted_modules[mod_name]: + del wanted_modules[mod_name] + break + + return found_symbols + + @staticmethod + def get_kernel_modules( + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + filter_modules: Optional[filter_modules_type], + ) -> Dict[str, List[Tuple[str, int, int]]]: + """ + Walks the kernel module list and finds the session layer, base, and size of each wanted module + """ + found_modules: Dict[str, List[Tuple[str, int, int]]] = {} + + if filter_modules: + # create a tuple of module names for use with `endswith` + filter_modules_check = tuple([key.lower() for key in filter_modules.keys()]) + else: + filter_modules_check = None + + session_layers = list( + modules.Modules.get_session_layers(context, layer_name, symbol_table) + ) + + # special handling for the kernel + gather_kernel = ( + filter_modules_check and PESymbols.os_module_name in filter_modules_check + ) + + for index, mod in enumerate( + modules.Modules.list_modules(context, layer_name, symbol_table) + ): + try: + mod_name = str(mod.BaseDllName.get_string().lower()) + except exceptions.InvalidAddressException: + continue + + # to analyze, it must either be the kernel or a wanted module + if not filter_modules_check or (gather_kernel and index == 0): + mod_name = PESymbols.os_module_name + elif filter_modules_check and not mod_name.endswith(filter_modules_check): + continue + + # we won't find symbol information if we can't analyze the module + session_layer_name = modules.Modules.find_session_layer( + context, session_layers, mod.DllBase + ) + if not session_layer_name: + continue + + if mod_name not in found_modules: + found_modules[mod_name] = [] + + found_modules[mod_name].append( + (session_layer_name, mod.DllBase, mod.SizeOfImage) + ) + + return found_modules + + @staticmethod + def get_process_modules( + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + filter_modules: Optional[filter_modules_type], + ) -> Dict[str, List[Tuple[str, int, int]]]: + """ + Walks the process list and each process' VAD to determine the base address and size of wanted modules + """ + proc_modules: Dict[str, List[Tuple[str, int, int]]] = {} + + if filter_modules: + # create a tuple of module names for use with `endswith` + filter_modules_check = tuple([key.lower() for key in filter_modules.keys()]) + else: + filter_modules_check = None + + for _, proc_layer_name, vads in vadinfo.VadInfo.get_all_vads_with_file_paths( + context, layer_name, symbol_table + ): + for vad_start, vad_size, filepath in vads: + filename = PESymbols.filename_for_path(filepath) + + if filter_modules_check and not filename.endswith(filter_modules_check): + continue + + # track each module along with the process layer and range to find it + if filename not in proc_modules: + proc_modules[filename] = [] + + proc_modules[filename].append((proc_layer_name, vad_start, vad_size)) + + return proc_modules + + def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: + kernel = self.context.modules[self.config["kernel"]] + + if self.config["symbol"]: + filter_module = { + self.config["module"].lower(): { + PESymbols.wanted_names: [self.config["symbol"]] + } + } + + elif self.config["address"]: + filter_module = { + self.config["module"].lower(): { + PESymbols.wanted_addresses: [self.config["address"]] + } + } + + else: + vollog.error("--address or --symbol must be specified") + return + + if self.config["source"] == "kernel": + module_resolver = self.get_kernel_modules + else: + module_resolver = self.get_process_modules + + collected_modules = module_resolver( + self.context, kernel.layer_name, kernel.symbol_table_name, filter_module + ) + + found_symbols = PESymbols.find_symbols( + self.context, self.config_path, filter_module, collected_modules + ) + + for module, symbols in found_symbols.items(): + for symbol, address in symbols: + yield (0, (module, symbol, format_hints.Hex(address))) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Module", str), + ("Symbol", str), + ("Address", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py new file mode 100644 index 0000000000..0438bc9e36 --- /dev/null +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -0,0 +1,183 @@ +import logging + +from typing import Dict, Tuple, List, Generator + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + + +class unhooked_system_calls(interfaces.plugins.PluginInterface): + """Looks for signs of Skeleton Key malware""" + + _required_framework_version = (2, 4, 0) + + system_calls = { + "ntdll.dll": { + pe_symbols.PESymbols.wanted_names: [ + "NtCreateThread", + "NtProtectVirtualMemory", + "NtReadVirtualMemory", + "NtOpenProcess", + "NtWriteFile", + "NtQueryVirtualMemory", + "NtAllocateVirtualMemory", + "NtWorkerFactoryWorkerReady", + "NtAcceptConnectPort", + "NtAddDriverEntry", + "NtAdjustPrivilegesToken", + "NtAlpcCreatePort", + "NtClose", + "NtCreateFile", + "NtCreateMutant", + "NtOpenFile", + "NtOpenIoCompletion", + "NtOpenJobObject", + "NtOpenKey", + "NtOpenKeyEx", + "NtOpenThread", + "NtOpenThreadToken", + "NtOpenThreadTokenEx", + "NtWriteVirtualMemory", + "NtTraceEvent", + "NtTranslateFilePath", + "NtUmsThreadYield", + "NtUnloadDriver", + "NtUnloadKey", + "NtUnloadKey2", + "NtUnloadKeyEx", + "NtCreateKey", + "NtCreateSection", + "NtDeleteKey", + "NtDeleteValueKey", + "NtDuplicateObject", + "NtQueryValueKey", + "NtReplaceKey", + "NtRequestWaitReplyPort", + "NtRestoreKey", + "NtSetContextThread", + "NtSetSecurityObject", + "NtSetValueKey", + "NtSystemDebugControl", + "NtTerminateProcess", + ] + } + } + + _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] + + @classmethod + def get_requirements(cls) -> List: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="pe_symbols", plugin=pe_symbols.PESymbols, version=(1, 0, 0) + ), + ] + + def _gather_code_bytes( + self, + kernel: interfaces.context.ModuleInterface, + found_symbols: pe_symbols.PESymbols.found_symbols_type, + ) -> _code_bytes_type: + """ + Enumerates the desired DLLs and function implementations in each process + Groups based on unique implementations of each DLLs' functions + The purpose is to detect when a function has different implementations (code) + in different processes. + This very effectively detects code injection. + """ + code_bytes: unhooked_system_calls._code_bytes_type = {} + + procs = pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) + + for proc in procs: + try: + proc_id = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + for dll_name, functions in found_symbols.items(): + for func_name, func_addr in functions: + try: + fbytes = self.context.layers[proc_layer_name].read( + func_addr, 0x20 + ) + except exceptions.InvalidAddressException: + continue + + if dll_name not in code_bytes: + code_bytes[dll_name] = {} + + if func_name not in code_bytes[dll_name]: + code_bytes[dll_name][func_name] = {} + + if fbytes not in code_bytes[dll_name][func_name]: + code_bytes[dll_name][func_name][fbytes] = [] + + code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name)) + + return code_bytes + + def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: + kernel = self.context.modules[self.config["kernel"]] + + found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + unhooked_system_calls.system_calls, + ) + + # code_bytes[dll_name][func_name][func_bytes] + code_bytes = self._gather_code_bytes(kernel, found_symbols) + + for functions in code_bytes.values(): + for func_name, cbb in functions.items(): + cb = list(cbb.values()) + + # same implementation in all + if len(cb) == 1: + yield 0, (func_name, "", len(cb[0])) + else: + # find the processes that are hooked for reporting + max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 + small_idx = (~max_idx) & 1 + + ps = [] + + for pid, pname in cb[small_idx]: + ps.append("{:d}:{}".format(pid, pname)) + + proc_names = ", ".join(ps) + + yield 0, (func_name, proc_names, len(cb[max_idx])) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Function", str), + ("Distinct Implementations", str), + ("Total Implementations", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index abc6142fe0..97a2f24559 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -3,13 +3,13 @@ # import logging -from typing import Callable, List, Generator, Iterable, Type, Optional +from typing import Callable, List, Generator, Iterable, Type, Optional, Tuple -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import renderers, interfaces, exceptions, symbols from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist +from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class VadInfo(interfaces.plugins.PluginInterface): _version = (2, 0, 0) MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs): # type: ignore super().__init__(*args, **kwargs) self._protect_values = None @@ -107,6 +107,58 @@ def protect_values( ) return values # type: ignore + @staticmethod + def get_proc_vads_with_file_paths( + proc: interfaces.objects.ObjectInterface, + ) -> pe_symbols.PESymbols.ranges_type: + """ + Returns a list of the process' vads that map a file + """ + vads = [] + + for vad in proc.get_vad_root().traverse(): + filepath = vad.get_file_name() + if not isinstance(filepath, str) or filepath.count("\\") == 0: + continue + + vads.append((vad.get_start(), vad.get_size(), filepath)) + + return vads + + @classmethod + def get_all_vads_with_file_paths( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table_name: str, + ) -> Generator[ + Tuple[ + interfaces.objects.ObjectInterface, str, pe_symbols.PESymbols.ranges_type + ], + None, + None, + ]: + """ + Yields each set of vads for a process that have a file mapped, along with the process itself and its layer + """ + is_32bit_arch = not symbols.symbol_table_is_64bit(context, symbol_table_name) + + procs = pslist.PsList.list_processes( + context=context, + layer_name=layer_name, + symbol_table=symbol_table_name, + ) + + for proc in procs: + try: + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + vads = cls.get_proc_vads_with_file_paths(proc) + + yield proc, proc_layer_name, vads + @classmethod def list_vads( cls, @@ -196,11 +248,33 @@ def vad_dump( return file_handle - def _generator(self, procs): + def _generator( + self, procs: List[interfaces.objects.ObjectInterface] + ) -> Generator[ + Tuple[ + int, + Tuple[ + int, + str, + format_hints.Hex, + format_hints.Hex, + format_hints.Hex, + str, + str, + int, + int, + format_hints.Hex, + str, + str, + ], + ], + None, + None, + ]: kernel = self.context.modules[self.config["kernel"]] kernel_layer = self.context.layers[kernel.layer_name] - def passthrough(_: interfaces.objects.ObjectInterface) -> bool: + def passthrough(x: interfaces.objects.ObjectInterface) -> bool: return False filter_func = passthrough @@ -250,7 +324,7 @@ def filter_function(x: interfaces.objects.ObjectInterface) -> bool: ), ) - def run(self): + def run(self) -> renderers.TreeGrid: kernel = self.context.modules[self.config["kernel"]] filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) From 3bb9264a5770d6828487129b6ebfec509ade8680 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:11:16 -0500 Subject: [PATCH 084/250] formatting --- volatility3/framework/plugins/windows/debugregisters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 5f3c2e5194..c70f922aeb 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -37,7 +37,7 @@ def get_requirements(cls) -> List: requirements.VersionRequirement( name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0) ), - ] + ] def _get_debug_info( self, ethread: interfaces.objects.ObjectInterface From 30cb5bd3ab4bac9f3ebb224d2f37cb98d6960545 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:14:06 -0500 Subject: [PATCH 085/250] Formatting that my local black doesn't understand --- volatility3/framework/plugins/windows/vadinfo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 97a2f24559..b3b7bf5fb9 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -249,8 +249,7 @@ def vad_dump( return file_handle def _generator( - self, procs: List[interfaces.objects.ObjectInterface] - ) -> Generator[ + self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[ Tuple[ int, Tuple[ From c223ac6e762d1072f04c21276645d7c2dda79dde Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:31:55 -0500 Subject: [PATCH 086/250] more black help --- volatility3/framework/plugins/windows/vadinfo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index b3b7bf5fb9..655e54be63 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -141,8 +141,6 @@ def get_all_vads_with_file_paths( """ Yields each set of vads for a process that have a file mapped, along with the process itself and its layer """ - is_32bit_arch = not symbols.symbol_table_is_64bit(context, symbol_table_name) - procs = pslist.PsList.list_processes( context=context, layer_name=layer_name, From 7b48ee4be489b4667c57cca55da84abd3c1c3d12 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:36:13 -0500 Subject: [PATCH 087/250] more black help --- volatility3/framework/plugins/windows/vadinfo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 655e54be63..594c0d67c5 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -246,8 +246,7 @@ def vad_dump( return file_handle - def _generator( - self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[ + def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[ Tuple[ int, Tuple[ From bcd93616c0628fdb991866290845bbb861889f8f Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:36:35 -0500 Subject: [PATCH 088/250] more black help --- volatility3/framework/plugins/windows/vadinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 594c0d67c5..e129ad4ef9 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -5,7 +5,7 @@ import logging from typing import Callable, List, Generator, Iterable, Type, Optional, Tuple -from volatility3.framework import renderers, interfaces, exceptions, symbols +from volatility3.framework import renderers, interfaces, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints From fd8777287736baee75e6f1a8843b471a47b6ced2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:47:42 -0500 Subject: [PATCH 089/250] Move VAD enumeration into pe_symbols --- .../framework/plugins/windows/pe_symbols.py | 57 +++++++++++++++++-- .../framework/plugins/windows/vadinfo.py | 50 ---------------- 2 files changed, 52 insertions(+), 55 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index b7faeaf555..51558f71bf 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -16,7 +16,7 @@ from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, vadinfo, modules +from volatility3.plugins.windows import pslist, modules vollog = logging.getLogger(__name__) @@ -170,9 +170,6 @@ def get_requirements(cls) -> List: requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(2, 0, 0) ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), requirements.VersionRequirement( name="modules", component=modules.Modules, version=(2, 0, 0) ), @@ -648,6 +645,56 @@ def get_kernel_modules( return found_modules + @staticmethod + def get_proc_vads_with_file_paths( + proc: interfaces.objects.ObjectInterface, + ) -> ranges_type: + """ + Returns a list of the process' vads that map a file + """ + vads = [] + + for vad in proc.get_vad_root().traverse(): + filepath = vad.get_file_name() + if not isinstance(filepath, str) or filepath.count("\\") == 0: + continue + + vads.append((vad.get_start(), vad.get_size(), filepath)) + + return vads + + @classmethod + def get_all_vads_with_file_paths( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table_name: str, + ) -> Generator[ + Tuple[ + interfaces.objects.ObjectInterface, str, ranges_type + ], + None, + None, + ]: + """ + Yields each set of vads for a process that have a file mapped, along with the process itself and its layer + """ + procs = pslist.PsList.list_processes( + context=context, + layer_name=layer_name, + symbol_table=symbol_table_name, + ) + + for proc in procs: + try: + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + vads = PESymbols.get_proc_vads_with_file_paths(proc) + + yield proc, proc_layer_name, vads + @staticmethod def get_process_modules( context: interfaces.context.ContextInterface, @@ -666,7 +713,7 @@ def get_process_modules( else: filter_modules_check = None - for _, proc_layer_name, vads in vadinfo.VadInfo.get_all_vads_with_file_paths( + for _, proc_layer_name, vads in PESymbols.get_all_vads_with_file_paths( context, layer_name, symbol_table ): for vad_start, vad_size, filepath in vads: diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index e129ad4ef9..974793a711 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -107,56 +107,6 @@ def protect_values( ) return values # type: ignore - @staticmethod - def get_proc_vads_with_file_paths( - proc: interfaces.objects.ObjectInterface, - ) -> pe_symbols.PESymbols.ranges_type: - """ - Returns a list of the process' vads that map a file - """ - vads = [] - - for vad in proc.get_vad_root().traverse(): - filepath = vad.get_file_name() - if not isinstance(filepath, str) or filepath.count("\\") == 0: - continue - - vads.append((vad.get_start(), vad.get_size(), filepath)) - - return vads - - @classmethod - def get_all_vads_with_file_paths( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table_name: str, - ) -> Generator[ - Tuple[ - interfaces.objects.ObjectInterface, str, pe_symbols.PESymbols.ranges_type - ], - None, - None, - ]: - """ - Yields each set of vads for a process that have a file mapped, along with the process itself and its layer - """ - procs = pslist.PsList.list_processes( - context=context, - layer_name=layer_name, - symbol_table=symbol_table_name, - ) - - for proc in procs: - try: - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException: - continue - - vads = cls.get_proc_vads_with_file_paths(proc) - - yield proc, proc_layer_name, vads - @classmethod def list_vads( cls, From 61cf58d97794359e3e093a97f3b8d2562661aea4 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 12:49:01 -0500 Subject: [PATCH 090/250] black fixes --- volatility3/framework/plugins/windows/pe_symbols.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 51558f71bf..10e87c552e 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -670,9 +670,7 @@ def get_all_vads_with_file_paths( layer_name: str, symbol_table_name: str, ) -> Generator[ - Tuple[ - interfaces.objects.ObjectInterface, str, ranges_type - ], + Tuple[interfaces.objects.ObjectInterface, str, ranges_type], None, None, ]: From 802fc024b5a7666f1e56a641af4fe219c856f6b4 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 6 Sep 2024 13:27:04 -0500 Subject: [PATCH 091/250] switch api place --- volatility3/framework/plugins/windows/debugregisters.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index c70f922aeb..931ccef37e 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -9,7 +9,6 @@ from volatility3.framework.renderers import format_hints import volatility3.plugins.windows.pslist as pslist import volatility3.plugins.windows.threads as threads -import volatility3.plugins.windows.vadinfo as vadinfo import volatility3.plugins.windows.pe_symbols as pe_symbols vollog = logging.getLogger(__name__) @@ -31,9 +30,6 @@ def get_requirements(cls) -> List: requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(2, 0, 0) ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), requirements.VersionRequirement( name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0) ), @@ -80,7 +76,7 @@ def _get_vads( if owner_proc.vol.offset in vads_cache: vads = vads_cache[owner_proc.vol.offset] else: - vads = vadinfo.VadInfo.get_proc_vads_with_file_paths(owner_proc) + vads = pe_symbols.PESymbols.get_proc_vads_with_file_paths(owner_proc) vads_cache[owner_proc.vol.offset] = vads # smear or terminated process From 26cd73115b58cf5b1adfb430c9f1070e45dbe727 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 13:31:09 +0100 Subject: [PATCH 092/250] CLI: Filter on rendered values --- volatility3/cli/text_renderer.py | 38 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index ab9e44141b..1f9cfa9d8c 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -179,7 +179,15 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd.write("\n{}\n".format("\t".join(line))) def visitor(node: interfaces.renderers.TreeNode, accumulator): - if self.filter and self.filter.filter(node.values): + line = [] + for column_index in range(len(grid.columns)): + column = grid.columns[column_index] + renderer = self._type_renderers.get( + column.type, self._type_renderers["default"] + ) + line.append(renderer(node.values[column_index])) + + if self.filter and self.filter.filter(line): return accumulator accumulator.write("\n") @@ -188,13 +196,6 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): "*" * max(0, node.path_depth - 1) + ("" if (node.path_depth <= 1) else " ") ) - line = [] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] - renderer = self._type_renderers.get( - column.type, self._type_renderers["default"] - ) - line.append(renderer(node.values[column_index])) accumulator.write("{}".format("\t".join(line))) accumulator.flush() return accumulator @@ -259,12 +260,18 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: def visitor(node: interfaces.renderers.TreeNode, accumulator): # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case row = {"TreeDepth": str(max(0, node.path_depth - 1))} + line = [] for column_index in range(len(grid.columns)): column = grid.columns[column_index] renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) row[f"{column.name}"] = renderer(node.values[column_index]) + line.append(row[f"{column.name}"]) + + if self.filter and self.filter.filter(line): + return accumulator + accumulator.writerow(row) return accumulator @@ -317,10 +324,8 @@ def visitor( max_column_widths.get(tree_indent_column, 0), node.path_depth ) - if self.filter and self.filter.filter(node.values): - return accumulator - line = {} + rendered_line = [] for column_index in range(len(grid.columns)): column = grid.columns[column_index] renderer = self._type_renderers.get( @@ -334,6 +339,11 @@ def visitor( max_column_widths.get(column.name, len(column.name)), field_width ) line[column] = data.split("\n") + rendered_line.append(data) + + if self.filter and self.filter.filter(rendered_line): + return accumulator + accumulator.append((node.path_depth, line)) return accumulator @@ -437,6 +447,7 @@ def visitor( # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case acc_map, final_tree = accumulator node_dict: Dict[str, Any] = {"__children": []} + line = [] for column_index in range(len(grid.columns)): column = grid.columns[column_index] renderer = self._type_renderers.get( @@ -446,6 +457,11 @@ def visitor( if isinstance(data, interfaces.renderers.BaseAbsentValue): data = None node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + if node.parent: acc_map[node.parent.path]["__children"].append(node_dict) else: From 8259ca90199f548e1592aa3ab05444c9353fe66a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 13:47:26 +0100 Subject: [PATCH 093/250] Core: Bump the framework number so we can differentiate CLI versions --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index d30446d63b..a131609c93 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,6 +1,6 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 9 # Number of changes that only add to the interface +VERSION_MINOR = 10 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From 4361c3857393b7f7311e9b600d1ea5d4966cdb93 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 14:55:04 +0100 Subject: [PATCH 094/250] Core: Verify plugin requirements of plugins --- .../framework/configuration/requirements.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 1c0622574e..931995bb8f 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -585,6 +585,24 @@ def __init__( version=version, ) + def unsatisfied( + self, context: interfaces.context.ContextInterface, config_path: str + ) -> Dict[str, interfaces.configuration.RequirementInterface]: + result = super().unsatisfied(context, config_path) + if not result: + component: Type[interfaces.plugins.PluginInterface] = self._component + for requirement in component.get_requirements(): + if isinstance(requirement, PluginRequirement): + result.update( + requirement.unsatisfied( + context, + interfaces.configuration.path_join(config_path, self.name), + ) + ) + if result: + result[config_path] = self + return result + class ModuleRequirement( interfaces.configuration.ConstructableRequirementInterface, From f3085b6c59b05190320722feef58f276357d2155 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 16:06:23 +0100 Subject: [PATCH 095/250] Core: Move the pluginrequirement check to generic versionrequirement --- .../framework/configuration/requirements.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 931995bb8f..f95dac3070 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -527,12 +527,14 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): def __init__( self, name: str, - description: str = None, + description: Optional[str] = None, default: bool = False, optional: bool = False, component: Type[interfaces.configuration.VersionableInterface] = None, version: Optional[Tuple[int, ...]] = None, ) -> None: + if description is None: + description = f"Version {".".join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet" super().__init__( name=name, description=description, default=default, optional=optional ) @@ -550,9 +552,29 @@ def unsatisfied( config_path = interfaces.configuration.path_join(config_path, self.name) if not self.matches_required(self._version, self._component.version): return {config_path: self} + + # Check for child requirements + if issubclass(self._component, interfaces.configuration.ConfigurableInterface): + result = {} + for requirement in self._component.get_requirements(): + if not requirement.optional and isinstance( + requirement, VersionRequirement + ): + result.update( + requirement.unsatisfied( + context, + config_path, + ) + ) + + if result: + result.update({config_path: self}) + return result + context.config[interfaces.configuration.path_join(config_path, self.name)] = ( True ) + return {} @classmethod @@ -585,24 +607,6 @@ def __init__( version=version, ) - def unsatisfied( - self, context: interfaces.context.ContextInterface, config_path: str - ) -> Dict[str, interfaces.configuration.RequirementInterface]: - result = super().unsatisfied(context, config_path) - if not result: - component: Type[interfaces.plugins.PluginInterface] = self._component - for requirement in component.get_requirements(): - if isinstance(requirement, PluginRequirement): - result.update( - requirement.unsatisfied( - context, - interfaces.configuration.path_join(config_path, self.name), - ) - ) - if result: - result[config_path] = self - return result - class ModuleRequirement( interfaces.configuration.ConstructableRequirementInterface, From ace590e8f0669797b5770773eeadcbcb27752a78 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 16:08:48 +0100 Subject: [PATCH 096/250] Core: Fix up f-string containing a string --- volatility3/framework/configuration/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index f95dac3070..3a862a132c 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -534,7 +534,7 @@ def __init__( version: Optional[Tuple[int, ...]] = None, ) -> None: if description is None: - description = f"Version {".".join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet" + description = f"Version {'.'.join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet" super().__init__( name=name, description=description, default=default, optional=optional ) From bf000ff0a0bafc0197ee1dd074f1df450362d02f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 21:42:59 +0100 Subject: [PATCH 097/250] Core: Add recursion protection to VersionRequirement check --- .../framework/configuration/requirements.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 3a862a132c..49ca49b59d 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -546,13 +546,26 @@ def __init__( self._version = version def unsatisfied( - self, context: interfaces.context.ContextInterface, config_path: str + self, + context: interfaces.context.ContextInterface, + config_path: str, + accumulator: Optional[ + List[interfaces.configuration.VersionableInterface] + ] = None, ) -> Dict[str, interfaces.configuration.RequirementInterface]: # Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type config_path = interfaces.configuration.path_join(config_path, self.name) if not self.matches_required(self._version, self._component.version): return {config_path: self} + if accumulator is None: + accumulator = set([self._component]) + else: + if self._component in accumulator: + return {config_path: self} + else: + accumulator.add(self._component) + # Check for child requirements if issubclass(self._component, interfaces.configuration.ConfigurableInterface): result = {} @@ -562,8 +575,7 @@ def unsatisfied( ): result.update( requirement.unsatisfied( - context, - config_path, + context, config_path, accumulator.copy() ) ) From 9152f33181fd347366468d00c47a2c22ae71cfc2 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Sep 2024 21:58:09 +0100 Subject: [PATCH 098/250] Core: Allow circular dependencies as long as they are all met --- volatility3/framework/configuration/requirements.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 49ca49b59d..dcb7505ead 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -558,16 +558,20 @@ def unsatisfied( if not self.matches_required(self._version, self._component.version): return {config_path: self} + recurse = True if accumulator is None: accumulator = set([self._component]) else: if self._component in accumulator: - return {config_path: self} + recurse = False else: accumulator.add(self._component) # Check for child requirements - if issubclass(self._component, interfaces.configuration.ConfigurableInterface): + if ( + issubclass(self._component, interfaces.configuration.ConfigurableInterface) + and recurse + ): result = {} for requirement in self._component.get_requirements(): if not requirement.optional and isinstance( From fd2c97e1bafb5b30403a1788eae4c32723438746 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 9 Sep 2024 00:44:00 +0100 Subject: [PATCH 099/250] CLI: Use enumerate for renderers (thanks @gcmoreira) --- volatility3/cli/text_renderer.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 1f9cfa9d8c..da0cdf62a7 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -180,8 +180,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: def visitor(node: interfaces.renderers.TreeNode, accumulator): line = [] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) @@ -261,8 +260,7 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case row = {"TreeDepth": str(max(0, node.path_depth - 1))} line = [] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) @@ -326,8 +324,7 @@ def visitor( line = {} rendered_line = [] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) @@ -357,8 +354,7 @@ def visitor( format_string_list = [ "{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}" ] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): format_string_list.append( "{" + str(column_index + 1) @@ -448,8 +444,7 @@ def visitor( acc_map, final_tree = accumulator node_dict: Dict[str, Any] = {"__children": []} line = [] - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) From b7e604d63f667663f9a00415493ec4f8b12a5024 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Tue, 10 Sep 2024 14:50:40 -0500 Subject: [PATCH 100/250] Address feedback --- .../plugins/windows/debugregisters.py | 33 +- .../framework/plugins/windows/pe_symbols.py | 392 ++++++++++++++---- .../plugins/windows/unhooked_system_calls.py | 7 +- .../framework/plugins/windows/vadinfo.py | 4 +- 4 files changed, 330 insertions(+), 106 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 931ccef37e..65b2e625b9 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -1,3 +1,6 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 + import logging from typing import Tuple, Optional, Generator, List, Dict @@ -35,11 +38,16 @@ def get_requirements(cls) -> List: ), ] + @staticmethod def _get_debug_info( - self, ethread: interfaces.objects.ObjectInterface + ethread: interfaces.objects.ObjectInterface, ) -> Optional[Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]]: """ Gathers information related to the debug registers for the given thread + Args: + ethread: the thread (_ETHREAD) to examine + Returns: + Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]: The owner process of the thread and the values for dr7, dr0, dr1, dr2, dr3 """ try: dr7 = ethread.Tcb.TrapFrame.Dr7 @@ -68,23 +76,6 @@ def _get_debug_info( return owner_proc, dr7, dr0, dr1, dr2, dr3 - def _get_vads( - self, - vads_cache: Dict[int, List[Tuple[int, int, str]]], - owner_proc: interfaces.objects.ObjectInterface, - ) -> Optional[List[Tuple[int, int, str]]]: - if owner_proc.vol.offset in vads_cache: - vads = vads_cache[owner_proc.vol.offset] - else: - vads = pe_symbols.PESymbols.get_proc_vads_with_file_paths(owner_proc) - vads_cache[owner_proc.vol.offset] = vads - - # smear or terminated process - if len(vads) == 0: - return None - - return vads - def _generator( self, ) -> Generator[ @@ -115,7 +106,7 @@ def _generator( ]: kernel = self.context.modules[self.config["kernel"]] - vads_cache: Dict[int, List[Tuple[int, int, str]]] = {} + vads_cache: Dict[int, pe_symbols.ranges_type] = {} proc_modules = None @@ -133,7 +124,9 @@ def _generator( owner_proc, dr7, dr0, dr1, dr2, dr3 = debug_info - vads = self._get_vads(vads_cache, owner_proc) + vads = pe_symbols.PESymbols.get_vads_for_process_cache( + vads_cache, owner_proc + ) if not vads: continue diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 10e87c552e..c9785a1c95 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -1,9 +1,9 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 - import io import logging +import ntpath from typing import Dict, Tuple, Optional, List, Generator, Union @@ -17,9 +17,37 @@ from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins.windows import pslist, modules +from volatility3.framework.constants.windows import KERNEL_MODULE_NAMES vollog = logging.getLogger(__name__) +# keys for specifying wanted names and/or addresses +# used for consistent access between the API and plugins +wanted_names_identifier = "names" +wanted_addresses_identifier = "addresses" + +# how wanted modules/symbols are specified, such as: +# {"ntdll.dll" : {wanted_addresses : [42, 43, 43]}} +# {"ntdll.dll" : {wanted_names : ["NtCreateThread"]}} +filter_modules_type = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] + +# holds resolved symbols +# {"ntdll.dll": [("Bob", 123), ("Alice", 456)]} +found_symbols_type = Dict[str, List[Tuple[str, int]]] + +# used to hold informatin about a range (VAD or kernel module) +# (start address, size, file path) +range_type = Tuple[int, int, str] +ranges_type = List[range_type] + +# collected_modules are modules and their symbols found when walking vads or kernel modules +# Tuple of (process or kernel layer name, range start, range size) +collected_module_instance = Tuple[str, int, int] +collected_modules_info = List[collected_module_instance] +collected_modules_type = Dict[str, collected_modules_info] + +PESymbolFinders = Union[interfaces.context.ModuleInterface, pefile.ExportDirData] + class PESymbolFinder: """ @@ -28,21 +56,19 @@ class PESymbolFinder: lookup symbols by name or address """ - cached_str = Union[str, None] - cached_str_dict = Dict[str, cached_str] + cached_str_dict = Dict[str, Optional[str]] - cached_int = Union[int, None] - cached_int_dict = Dict[str, cached_int] + cached_int_dict = Dict[str, Optional[int]] cached_value = Union[int, str, None] - cached_value_dict = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] + cached_value_dict = Dict[str, Dict[str, List[str]] | Dict[str, List[int]]] def __init__( self, layer_name: str, mod_name: str, module_start: int, - symbol_module: Union[interfaces.context.ModuleInterface, pefile.ExportDirData], + symbol_module: PESymbolFinders, ): self._layer_name = layer_name self._mod_name = mod_name @@ -56,10 +82,25 @@ def _get_cache_key(self, value: cached_value) -> str: """ Maintain a cache for symbol lookups to avoid re-walking of PDB symbols or export tables within the same module for the same address in the same layer + + Args: + value: The value (address or name) being cached + + Returns: + str: The constructed cache key that includes the layer and module name """ return f"{self._layer_name}|{self._mod_name}|{value}" - def get_name_for_address(self, address: int) -> cached_str: + def get_name_for_address(self, address: int) -> Optional[str]: + """ + Returns the name for the given address within the particular layer and module + + Args: + address: the address to resolve within the module + + Returns: + str: the name of the symbol, if found + """ cached_key = self._get_cache_key(address) if cached_key not in self._name_cache: name = self._do_get_name(address) @@ -67,7 +108,16 @@ def get_name_for_address(self, address: int) -> cached_str: return self._name_cache[cached_key] - def get_address_for_name(self, name: str) -> cached_int: + def get_address_for_name(self, name: str) -> Optional[int]: + """ + Returns the name for the given address within the particular layer and module + + Args: + str: the name of the symbol to resolve + + Returns: + address: the address of the symbol, if found + """ cached_key = self._get_cache_key(name) if cached_key not in self._address_cache: address = self._do_get_address(name) @@ -75,10 +125,30 @@ def get_address_for_name(self, name: str) -> cached_int: return self._address_cache[cached_key] - def _do_get_name(self, address: int) -> cached_str: + def _do_get_name(self, address: int) -> Optional[str]: + """ + Returns the name for the given address within the particular layer and module. + This method must be overwritten by sub classes. + + Args: + address: the address to resolve within the module + + Returns: + str: the name of the symbol, if found + """ raise NotImplementedError("_do_get_name must be overwritten") - def _do_get_address(self, name: str) -> cached_int: + def _do_get_address(self, name: str) -> Optional[int]: + """ + Returns the name for the given address within the particular layer and module + This method must be overwritten by sub classes. + + Args: + str: the name of the symbol to resolve + + Returns: + address: the address of the symbol, if found + """ raise NotImplementedError("_do_get_address must be overwritten") @@ -87,13 +157,31 @@ class PDBSymbolFinder(PESymbolFinder): PESymbolFinder implementation for PDB modules """ - def _do_get_address(self, name: str) -> PESymbolFinder.cached_int: + def _do_get_address(self, name: str) -> Optional[int]: + """ + _do_get_address implementation for PDBSymbolFinder + + Args: + str: the name of the symbol to resolve + + Returns: + address: the address of the symbol, if found + """ try: return self._symbol_module.get_absolute_symbol_address(name) except exceptions.SymbolError: return None - def _do_get_name(self, address: int) -> PESymbolFinder.cached_str: + def _do_get_name(self, address: int) -> Optional[str]: + """ + _do_get_name implementation for PDBSymbolFinder + + Args: + address: the address to resolve within the module + + Returns: + str: the name of the symbol, if found + """ try: name = self._symbol_module.get_symbols_by_absolute_location(address)[0] return name.split(constants.BANG)[1] @@ -113,14 +201,32 @@ def _get_name(self, export: pefile.ExportData) -> Optional[str]: except AttributeError: return None - def _do_get_name(self, address: int) -> PESymbolFinder.cached_str: + def _do_get_name(self, address: int) -> Optional[str]: + """ + _do_get_name implementation for ExportSymbolFinder + + Args: + address: the address to resolve within the module + + Returns: + str: the name of the symbol, if found + """ for export in self._symbol_module: if export.address + self._module_start == address: return self._get_name(export) return None - def _do_get_address(self, name: str) -> PESymbolFinder.cached_int: + def _do_get_address(self, name: str) -> Optional[int]: + """ + _do_get_address implementation for ExportSymbolFinder + Args: + str: the name of the symbol to resolve + + Returns: + address: the address of the symbol, if found + """ + for export in self._symbol_module: sym_name = self._get_name(export) if sym_name and sym_name == name: @@ -139,25 +245,6 @@ class PESymbols(interfaces.plugins.PluginInterface): # used for special handling of the kernel PDB file. See later notes os_module_name = "ntoskrnl.exe" - # keys for specifying wanted names and/or addresses - # used for consistent access between the API and plugins - wanted_names = "names" - wanted_addresses = "addresses" - - # how wanted modules/symbols are specified, such as: - # {"ntdll.dll" : {wanted_addresses : [42, 43, 43]}} - # {"ntdll.dll" : {wanted_names : ["NtCreateThread"]}} - filter_modules_type = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] - - # holds resolved symbols - # {"ntdll.dll": [("Bob", 123), ("Alice", 456)]} - found_symbols_type = Dict[str, List[Tuple[str, int]]] - - # used to hold informatin about a range (VAD or kernel module) - # (start address, size, file path) - range_type = Tuple[int, int, str] - ranges_type = List[range_type] - @classmethod def get_requirements(cls) -> List: # Since we're calling the plugin, make sure we have the plugin's requirements @@ -187,13 +274,15 @@ def get_requirements(cls) -> List: description='Module in which to resolve symbols. Use "ntoskrnl.exe" to resolve in the base kernel executable.', optional=False, ), - requirements.StringRequirement( - name="symbol", + requirements.ListRequirement( + name="symbols", + element_type=str, description="Symbol name to resolve", optional=True, ), - requirements.IntRequirement( - name="address", + requirements.ListRequirement( + name="addresses", + element_type=int, description="Address of symbol to resolve", optional=True, ), @@ -242,7 +331,15 @@ def range_info_for_address( ranges: ranges_type, address: int ) -> Optional[range_type]: """ - Helper for getting the range information for an address + Helper for getting the range information for an address. + Finds the range holding the `address` parameter + + Args: + address: the address to find the range for + + Returns: + Tuple[int, int, str]: The starting address, size, and file path of the range + """ for start, size, filepath in ranges: if start <= address < start + size: @@ -254,6 +351,13 @@ def range_info_for_address( def filepath_for_address(ranges: ranges_type, address: int) -> Optional[str]: """ Helper to get the file path for an address + + Args: + ranges: The set of VADs with mapped files to find the address + address: The address to find inside of the VADs set + + Returns: + str: The full path of the file, if found and present """ info = PESymbols.range_info_for_address(ranges, address) if info: @@ -264,9 +368,15 @@ def filepath_for_address(ranges: ranges_type, address: int) -> Optional[str]: @staticmethod def filename_for_path(filepath: str) -> str: """ - Consistent way to get the filename + Consistent way to get the filename regardless of platform + + Args: + str: the file path from `filepath_for_address` + + Returns: + str: the bsae file name of the full path """ - return filepath.split("\\")[-1] + return ntpath.basename(filepath) @staticmethod def addresses_for_process_symbols( @@ -276,6 +386,18 @@ def addresses_for_process_symbols( symbol_table_name: str, symbols: filter_modules_type, ) -> found_symbols_type: + """ + Used to easily resolve the addresses of names inside of modules. + + See the usage of this function for system call resolution in unhooked_system_calls.py + for an easy to understand example. + + Args: + symbols: The dictionary of symbols requested by the caller + + Returns: + found_symbols_type: The dictionary of symbols that were resolved + """ collected_modules = PESymbols.get_process_modules( context, layer_name, symbol_table_name, symbols ) @@ -294,16 +416,22 @@ def addresses_for_process_symbols( def path_and_symbol_for_address( context: interfaces.context.ContextInterface, config_path: str, - collected_modules: Dict[str, List[Tuple[str, int, int]]], + collected_modules: collected_modules_type, ranges: ranges_type, address: int, ) -> Tuple[str, str]: """ Method for plugins to determine the file path and symbol name for a given address - collected_modules: return value from `get_kernel_modules` or `get_process_modules` - ranges: the memory ranges to examine in this layer. - address: address to resolve to its symbol name + See debugregisters.py for an example of how this function is used along with get_vads_for_process_cache + for resolving symbols in processes. + + Args: + collected_modules: return value from `get_kernel_modules` or `get_process_modules` + ranges: the memory ranges to examine in this layer. + address: address to resolve to its symbol name + Returns: + Tuple[str|renderers.NotApplicableValue|renderers.NotAvailableValue, str|renderers.NotApplicableValue|renderers.NotAvailableValue] """ if not address: @@ -317,15 +445,15 @@ def path_and_symbol_for_address( filename = PESymbols.filename_for_path(filepath).lower() # setup to resolve the address - filter_module: PESymbols.filter_modules_type = { - filename: {PESymbols.wanted_addresses: [address]} + filter_module: filter_modules_type = { + filename: {wanted_addresses_identifier: [address]} } found_symbols = PESymbols.find_symbols( context, config_path, filter_module, collected_modules ) - if not found_symbols or not found_symbols[filename]: + if not found_symbols or filename not in found_symbols: return renderers.NotAvailableValue(), renderers.NotAvailableValue() return filepath, found_symbols[filename][0][0] @@ -335,13 +463,18 @@ def _get_exported_symbols( context: interfaces.context.ContextInterface, pe_table_name: str, mod_name: str, - module_info: Tuple[str, int, int], + module_info: collected_module_instance, ) -> Optional[ExportSymbolFinder]: """ Attempts to locate symbols based on export analysis - mod_name: lower case name of the module to resolve symbols in - module_info: (layer_name, module_start, module_size) of the module to examine + Args: + mod_name: lower case name of the module to resolve symbols in + module_info: (layer_name, module_start, module_size) of the module to examine + + Returns: + Optional[ExportSymbolFinder]: If the export table can be resolved, then the ExportSymbolFinder + instance for it """ layer_name = module_info[0] @@ -361,7 +494,10 @@ def _get_exported_symbols( return None return ExportSymbolFinder( - layer_name, mod_name, module_start, pe_module.DIRECTORY_ENTRY_EXPORT.symbols + layer_name, + mod_name.lower(), + module_start, + pe_module.DIRECTORY_ENTRY_EXPORT.symbols, ) @staticmethod @@ -369,13 +505,17 @@ def _get_pdb_module( context: interfaces.context.ContextInterface, config_path: str, mod_name: str, - module_info: Tuple[str, int, int], + module_info: collected_module_instance, ) -> Optional[PDBSymbolFinder]: """ - Attempts to locate symbols based on PDB analysis + Attempts to locate symbols based on PDB analysis through each layer where the mod_name module was found + + Args: + mod_name: lower case name of the module to resolve symbols in + module_info: (layer_name, module_start, module_size) of the module to examine - mod_name: lower case name of the module to resolve symbols in - module_info: (layer_name, module_start, module_size) of the module to examine + Returns: + Optional[PDBSymbolFinder]: If the export table can be resolved, then the ExportSymbolFinder """ mod_symbols = None @@ -386,15 +526,17 @@ def _get_pdb_module( # a `ntoskrnl.exe` can have an internal PDB name of any of the ones in the following list # The code attempts to find all possible PDBs to ensure the best chance of recovery if mod_name == PESymbols.os_module_name: - pdb_names = ["ntkrnlmp.pdb", "ntkrnlpa.pdb", "ntkrpamp.pdb", "ntoskrnl.pdb"] + pdb_names = [fn + ".pdb" for fn in KERNEL_MODULE_NAMES] # for non-kernel files, replace the exe, sys, or dll extension with pdb else: + # in testing we found where some DLLs, such amsi.dll, have its PDB string as Amsi.dll + # in certain Windows versions mod_name = mod_name[:-3] + "pdb" first_upper = mod_name[0].upper() + mod_name[1:] pdb_names = [mod_name, first_upper] - # loop through each PDB name (will be just one for all but the kernel) + # loop through each PDB name (all the kernel names or the dll name as lower() + first char upper case) for pdb_name in pdb_names: try: mod_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( @@ -433,11 +575,17 @@ def _get_pdb_module( def _find_symbols_through_pdb( context: interfaces.context.ContextInterface, config_path: str, - module_instances: List[Tuple[str, int, int]], + module_instances: collected_modules_info, mod_name: str, ) -> Generator[PDBSymbolFinder, None, None]: """ - Attempts to resolve the symbols in `wanted_symbols` through PDB analysis + Attempts to resolve the symbols in `mod_name` through PDB analysis + + Args: + module_instances: the set of layers in which the module was found + mod_name: name of the module to resolve symbols in + Returns: + Generator[PDBSymbolFinder]: a PDBSymbolFinder instance for each layer in which the module was found """ for module_info in module_instances: mod_module = PESymbols._get_pdb_module( @@ -450,11 +598,17 @@ def _find_symbols_through_pdb( def _find_symbols_through_exports( context: interfaces.context.ContextInterface, config_path: str, - module_instances: List[Tuple[str, int, int]], + module_instances: collected_modules_info, mod_name: str, ) -> Generator[ExportSymbolFinder, None, None]: """ - Attempts to resolve the symbols in `wanted_symbols` through export analysis + Attempts to resolve the symbols in `mod_name` through export analysis + + Args: + module_instances: the set of layers in which the module was found + mod_name: name of the module to resolve symbols in + Returns: + Generator[ExportSymbolFinder]: an ExportSymbolFinder instance for each layer in which the module was found """ pe_table_name = intermed.IntermediateSymbolTable.create( context, config_path, "windows", "pe", class_types=pe.class_types @@ -476,12 +630,21 @@ def _get_symbol_value( ) -> Generator[Tuple[str, int], None, None]: """ Enumerates the symbols specified as wanted by the calling plugin + + removes entries from wanted_modules as they found to avoid PDB or export analysis after resolving all symbols + + Args: + wanted_modules: the dictionary of modules and symbols to resolve. Modified to remove symbols as they are resolved. + mod_name: the name of module to resolve symbols in + + Returns: + Tuple[str, int]: the name and address of resolved symbols """ wanted_symbols = wanted_modules[mod_name] if ( - PESymbols.wanted_names not in wanted_symbols - and PESymbols.wanted_addresses not in wanted_symbols + wanted_names_identifier not in wanted_symbols + and wanted_addresses_identifier not in wanted_symbols ): vollog.warning( f"Invalid `wanted_symbols` sent to `find_symbols` for module {mod_name}. addresses and names keys both misssing." @@ -489,8 +652,8 @@ def _get_symbol_value( return symbol_keys = [ - (PESymbols.wanted_names, "get_address_for_name"), - (PESymbols.wanted_addresses, "get_name_for_address"), + (wanted_names_identifier, "get_address_for_name"), + (wanted_addresses_identifier, "get_name_for_address"), ] for symbol_key, symbol_getter in symbol_keys: @@ -503,7 +666,7 @@ def _get_symbol_value( ) if symbol_value: # yield out symbol name, symbol address - if symbol_key == PESymbols.wanted_names: + if symbol_key == wanted_names_identifier: yield wanted_value, symbol_value # type: ignore else: yield symbol_value, wanted_value # type: ignore @@ -521,13 +684,20 @@ def _get_symbol_value( def _resolve_symbols_through_methods( context: interfaces.context.ContextInterface, config_path: str, - module_instances: List[Tuple[str, int, int]], + module_instances: collected_modules_info, wanted_modules: PESymbolFinder.cached_value_dict, mod_name: str, ) -> Generator[Tuple[str, int], None, None]: """ Attempts to resolve every wanted symbol in `mod_name` Every layer is enumerated for maximum chance of recovery + + Args: + module_instances: the set of layers in which the module was found + wanted_modules: The symbols to resolve tied to their module names + mod_name: name of the module to resolve symbols in + Returns: + Generator[Tuple[str, int]]: resolved symbol names and addresses """ symbol_resolving_methods = [ PESymbols._find_symbols_through_pdb, @@ -554,13 +724,19 @@ def find_symbols( context: interfaces.context.ContextInterface, config_path: str, wanted_modules: PESymbolFinder.cached_value_dict, - collected_modules: Dict[str, List[Tuple[str, int, int]]], + collected_modules: collected_modules_type, ) -> found_symbols_type: """ Loops through each method of symbol analysis until each wanted symbol is found Returns the resolved symbols as a dictionary that includes the name and runtime address + + Args: + wanted_modules: the dictionary of modules and symbols to resolve. Modified to remove symbols as they are resolved. + collected_modules: return value from `get_kernel_modules` or `get_process_modules` + Returns: + found_symbols_type: The set of symbols resolved to their name and/or address """ - found_symbols: PESymbols.found_symbols_type = {} + found_symbols: found_symbols_type = {} for mod_name in wanted_modules: if mod_name not in collected_modules: @@ -594,11 +770,16 @@ def get_kernel_modules( layer_name: str, symbol_table: str, filter_modules: Optional[filter_modules_type], - ) -> Dict[str, List[Tuple[str, int, int]]]: + ) -> collected_modules_type: """ Walks the kernel module list and finds the session layer, base, and size of each wanted module + + Args: + filter_modules: The modules to filter the gathering to. If left as None, all kernel modules are gathered. + Returns: + collected_modules_type: The collection of modules found with at least one layer present """ - found_modules: Dict[str, List[Tuple[str, int, int]]] = {} + found_modules: collected_modules_type = {} if filter_modules: # create a tuple of module names for use with `endswith` @@ -645,16 +826,55 @@ def get_kernel_modules( return found_modules + @staticmethod + def get_vads_for_process_cache( + vads_cache: Dict[int, ranges_type], + owner_proc: interfaces.objects.ObjectInterface, + ) -> Optional[ranges_type]: + """ + Creates and utilizes a cache of a process' VADs for efficient lookups + + Returns the vad information of the VAD hosting the address, if found + + Args: + vads_cache: The existing cache of VADs + owner_proc: The process being inspected + Returns: + Optional[ranges_type]: The range holding the address, if found + """ + if owner_proc.vol.offset in vads_cache: + vads = vads_cache[owner_proc.vol.offset] + else: + vads = PESymbols.get_proc_vads_with_file_paths(owner_proc) + vads_cache[owner_proc.vol.offset] = vads + + # smear or terminated process + if len(vads) == 0: + return None + + return vads + @staticmethod def get_proc_vads_with_file_paths( proc: interfaces.objects.ObjectInterface, ) -> ranges_type: """ Returns a list of the process' vads that map a file + + Args: + proc: The process to gather the VADs for + + Returns: + ranges_type: The list of VADs for this process that map a file """ - vads = [] + vads: ranges_type = [] - for vad in proc.get_vad_root().traverse(): + try: + vad_root = proc.get_vad_root() + except exceptions.InvalidAddressException: + return vads + + for vad in vad_root.traverse(): filepath = vad.get_file_name() if not isinstance(filepath, str) or filepath.count("\\") == 0: continue @@ -676,6 +896,9 @@ def get_all_vads_with_file_paths( ]: """ Yields each set of vads for a process that have a file mapped, along with the process itself and its layer + + Args: + Generator[Tuple[interfaces.objects.ObjectInterface, str, ranges_type]]: Yields tuple of process objects, layers, and VADs mapping files """ procs = pslist.PsList.list_processes( context=context, @@ -689,7 +912,7 @@ def get_all_vads_with_file_paths( except exceptions.InvalidAddressException: continue - vads = PESymbols.get_proc_vads_with_file_paths(proc) + vads = cls.get_proc_vads_with_file_paths(proc) yield proc, proc_layer_name, vads @@ -699,11 +922,16 @@ def get_process_modules( layer_name: str, symbol_table: str, filter_modules: Optional[filter_modules_type], - ) -> Dict[str, List[Tuple[str, int, int]]]: + ) -> collected_modules_type: """ Walks the process list and each process' VAD to determine the base address and size of wanted modules + + Args: + filter_modules: The modules to filter the gathering to. If left as None, all process modules are gathered. + Returns: + collected_modules_type: The collection of modules found with at least one layer present """ - proc_modules: Dict[str, List[Tuple[str, int, int]]] = {} + proc_modules: collected_modules_type = {} if filter_modules: # create a tuple of module names for use with `endswith` @@ -711,7 +939,7 @@ def get_process_modules( else: filter_modules_check = None - for _, proc_layer_name, vads in PESymbols.get_all_vads_with_file_paths( + for _proc, proc_layer_name, vads in PESymbols.get_all_vads_with_file_paths( context, layer_name, symbol_table ): for vad_start, vad_size, filepath in vads: @@ -731,17 +959,17 @@ def get_process_modules( def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: kernel = self.context.modules[self.config["kernel"]] - if self.config["symbol"]: + if self.config["symbols"]: filter_module = { self.config["module"].lower(): { - PESymbols.wanted_names: [self.config["symbol"]] + wanted_names_identifier: self.config["symbols"] } } - elif self.config["address"]: + elif self.config["addresses"]: filter_module = { self.config["module"].lower(): { - PESymbols.wanted_addresses: [self.config["address"]] + wanted_addresses_identifier: self.config["addresses"] } } diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 0438bc9e36..68f4c4b80a 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -1,3 +1,6 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 + import logging from typing import Dict, Tuple, List, Generator @@ -18,7 +21,7 @@ class unhooked_system_calls(interfaces.plugins.PluginInterface): system_calls = { "ntdll.dll": { - pe_symbols.PESymbols.wanted_names: [ + pe_symbols.wanted_names_identifier: [ "NtCreateThread", "NtProtectVirtualMemory", "NtReadVirtualMemory", @@ -90,7 +93,7 @@ def get_requirements(cls) -> List: def _gather_code_bytes( self, kernel: interfaces.context.ModuleInterface, - found_symbols: pe_symbols.PESymbols.found_symbols_type, + found_symbols: pe_symbols.found_symbols_type, ) -> _code_bytes_type: """ Enumerates the desired DLLs and function implementations in each process diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 974793a711..2c6ed4dafc 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -9,7 +9,7 @@ from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, pe_symbols +from volatility3.plugins.windows import pslist vollog = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class VadInfo(interfaces.plugins.PluginInterface): _version = (2, 0, 0) MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb - def __init__(self, *args, **kwargs): # type: ignore + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._protect_values = None From b40c20dd7fe0d1e348f94531c290a9aceee34dcb Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Tue, 10 Sep 2024 14:55:00 -0500 Subject: [PATCH 101/250] Revert back to union to avoid failed tests --- volatility3/framework/plugins/windows/pe_symbols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index c9785a1c95..30e9b49d16 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -61,7 +61,7 @@ class PESymbolFinder: cached_int_dict = Dict[str, Optional[int]] cached_value = Union[int, str, None] - cached_value_dict = Dict[str, Dict[str, List[str]] | Dict[str, List[int]]] + cached_value_dict = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] def __init__( self, From c86d973329e1795ceb0be15e70d9411a38b0f60e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 11 Sep 2024 15:54:17 +1000 Subject: [PATCH 102/250] Linux page cache plugins pointer verification improvements --- .../framework/plugins/linux/mountinfo.py | 11 +++++--- .../framework/plugins/linux/pagecache.py | 27 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 1eaec77bf0..2499f009e2 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface): _required_framework_version = (2, 2, 0) - _version = (1, 2, 0) + _version = (1, 2, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -272,11 +272,16 @@ def get_superblocks( continue sb_ptr = mnt.get_mnt_sb() - if not sb_ptr or sb_ptr in seen_sb_ptr: + if not (sb_ptr and sb_ptr.is_readable()): + continue + + if sb_ptr in seen_sb_ptr: continue seen_sb_ptr.add(sb_ptr) - yield sb_ptr.dereference(), path_root + superblock = sb_ptr.dereference() + + yield superblock, path_root def run(self): pids = self.config.get("pids") diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index b6c5f7cc08..9b6f05c4d0 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -104,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -186,8 +186,12 @@ def _walk_dentry( seen_dentries.add(dentry_addr) - inode = dentry.d_inode - if not (inode and inode.is_valid()): + inode_ptr = dentry.d_inode + if not (inode_ptr and inode_ptr.is_readable()): + continue + + inode = inode_ptr.dereference() + if not inode.is_valid(): continue # This allows us to have consistent paths @@ -242,8 +246,9 @@ def get_inodes( # More dentry/inode sanity checks root_inode_ptr = root_dentry.d_inode - if not root_inode_ptr: + if not (root_inode_ptr and root_inode_ptr.is_readable()): continue + root_inode = root_inode_ptr.dereference() if not root_inode.is_valid(): continue @@ -269,10 +274,12 @@ def get_inodes( ): if not file_dentry: continue + # Dentry/inode sanity checks file_inode_ptr = file_dentry.d_inode - if not file_inode_ptr: + if not (file_inode_ptr and file_inode_ptr.is_readable()): continue + file_inode = file_inode_ptr.dereference() if not file_inode.is_valid(): continue @@ -382,7 +389,7 @@ class InodePages(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -482,15 +489,15 @@ def _generator(self): vollog.error("You must use either --inode or --find") return + if not inode.is_valid(): + vollog.error("Invalid inode at 0x%x", inode.vol.offset) + return + if not inode.is_reg: vollog.error("The inode is not a regular file") return inode_size = inode.i_size - if not inode.is_valid(): - vollog.error("Invalid inode at 0x%x", self.config["inode"]) - return - for page_obj in inode.get_pages(): page_vaddr = page_obj.vol.offset page_paddr = page_obj.to_paddr() From 037eb1ce036ae7dea48c427742ae889c9b2ec7a3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 11 Sep 2024 16:30:41 +1000 Subject: [PATCH 103/250] Linux Check_creds plugins pointer verification improvements --- volatility3/framework/plugins/linux/check_creds.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index ab6ee4935c..45df966d2b 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -16,6 +16,8 @@ class Check_creds(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) + @classmethod def get_requirements(cls): return [ @@ -46,7 +48,11 @@ def _generator(self): tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) for task in tasks: - cred_addr = task.cred.dereference().vol.offset + task_cred_ptr = task.cred + if not (task_cred_ptr and task_cred_ptr.is_readable()): + continue + + cred_addr = task_cred_ptr.dereference().vol.offset if cred_addr not in creds: creds[cred_addr] = [] From c77c662b70c6751087bf947c400a045c81e7a8ec Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 11 Sep 2024 21:09:08 +1000 Subject: [PATCH 104/250] Linux pidhashtable plugin pointer verification improvements --- .../framework/plugins/linux/pidhashtable.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index 3223aed4a3..edafe97e05 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -20,7 +20,7 @@ class PIDHashTable(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -45,9 +45,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] def _is_valid_task(self, task) -> bool: - vmlinux = self.context.modules[self.config["kernel"]] - vmlinux_layer = self.context.layers[vmlinux.layer_name] - return bool(task and task.pid > 0 and vmlinux_layer.is_valid(task.parent)) + return bool(task and task.pid > 0 and task.parent.is_readable()) def _get_pidtype_pid(self): vmlinux = self.context.modules[self.config["kernel"]] @@ -96,7 +94,7 @@ def _walk_upid(self, seen_upids, upid): seen_upids.add(upid.vol.offset) pid_chain = upid.pid_chain - if not (pid_chain and vmlinux_layer.is_valid(pid_chain.vol.offset)): + if not (pid_chain.next and pid_chain.next.is_readable()): break upid = linux.LinuxUtilities.container_of( @@ -105,7 +103,6 @@ def _walk_upid(self, seen_upids, upid): def _get_upids(self): vmlinux = self.context.modules[self.config["kernel"]] - vmlinux_layer = self.context.layers[vmlinux.layer_name] # 2.6.24 <= kernels < 4.15 pidhash = self._get_pidhash_array() @@ -115,7 +112,7 @@ def _get_upids(self): # each entry in the hlist is a upid which is wrapped in a pid ent = hlist.first - while ent and vmlinux_layer.is_valid(ent.vol.offset): + while ent and ent.is_readable(): # upid->pid_chain exists 2.6.24 <= kernel < 4.15 upid = linux.LinuxUtilities.container_of( ent.vol.offset, "upid", "pid_chain", vmlinux @@ -143,7 +140,7 @@ def _pid_hash_implementation(self): continue pid_tasks_0 = pid.tasks[pidtype_pid].first - if not pid_tasks_0: + if not (pid_tasks_0 and pid_tasks_0.is_readable()): continue task = vmlinux.object( @@ -160,7 +157,7 @@ def _task_for_radix_pid_node(self, nodep): pidtype_pid = self._get_pidtype_pid() pid_tasks_0 = pid.tasks[pidtype_pid].first - if not pid_tasks_0: + if not (pid_tasks_0 and pid_tasks_0.is_readable()): return None task_struct_type = vmlinux.get_type("task_struct") From 9e8471799adcf090e20ea98a20309076193e9009 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 10:44:43 +1000 Subject: [PATCH 105/250] Improving code and adding the credential virtual addresses to the output. --- .../framework/plugins/linux/check_creds.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index 45df966d2b..3e292ae33f 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -2,21 +2,18 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import logging - from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.plugins.linux import pslist -vollog = logging.getLogger(__name__) - class Check_creds(interfaces.plugins.PluginInterface): """Checks if any processes are sharing credential structures""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 1) + _version = (1, 1, 0) @classmethod def get_requirements(cls): @@ -54,18 +51,22 @@ def _generator(self): cred_addr = task_cred_ptr.dereference().vol.offset - if cred_addr not in creds: - creds[cred_addr] = [] - + creds.setdefault(cred_addr, []) creds[cred_addr].append(task.pid) - for _, pids in creds.items(): + for cred_addr, pids in creds.items(): if len(pids) > 1: - pid_str = "" - for pid in pids: - pid_str = pid_str + f"{pid:d}, " - pid_str = pid_str[:-2] - yield (0, [str(pid_str)]) + pid_str = ", ".join([str(pid) for pid in pids]) + + fields = [ + format_hints.Hex(cred_addr), + pid_str, + ] + yield (0, fields) def run(self): - return renderers.TreeGrid([("PIDs", str)], self._generator()) + headers = [ + ("CredVAddr", format_hints.Hex), + ("PIDs", str), + ] + return renderers.TreeGrid(headers, self._generator()) From 57de357ffdfe87dbcad8c219228a4a0d0e17c173 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 16:05:40 +1000 Subject: [PATCH 106/250] Timeliner plugin: Fix issue with filtering TimeLinerInterface plugins and using the filter argument --- volatility3/framework/plugins/timeliner.py | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index f657a29183..abe802e8fb 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -45,6 +45,7 @@ class Timeliner(interfaces.plugins.PluginInterface): orders the results by time.""" _required_framework_version = (2, 0, 0) + _version = (1, 1, 0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -245,6 +246,17 @@ def run(self): filter_list = self.config["plugin-filter"] # Identify plugins that we can run which output datetimes for plugin_class in self.usable_plugins: + if not issubclass(plugin_class, TimeLinerInterface): + continue + + if filter_list and not any( + [ + filter in plugin_class.__module__ + "." + plugin_class.__name__ + for filter in filter_list + ] + ): + continue + try: automagics = automagic.choose_automagic(self.automagics, plugin_class) @@ -276,15 +288,8 @@ def run(self): config_value, ) - if isinstance(plugin, TimeLinerInterface): - if not len(filter_list) or any( - [ - filter - in plugin.__module__ + "." + plugin.__class__.__name__ - for filter in filter_list - ] - ): - plugins_to_run.append(plugin) + plugins_to_run.append(plugin) + except exceptions.UnsatisfiedException as excp: # Remove the failed plugin from the list and continue vollog.debug( From be05ace29b134156fe5f7584921887426fc2f41f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 16:06:40 +1000 Subject: [PATCH 107/250] Timeliner plugin: Add exception information --- volatility3/framework/plugins/timeliner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index abe802e8fb..56fe465e49 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -199,9 +199,10 @@ def _generator( ), ) ) - except Exception: + except Exception as e: vollog.log( - logging.INFO, f"Exception occurred running plugin: {plugin_name}" + logging.INFO, + f"Exception occurred running plugin: {plugin_name}: {e}", ) vollog.log(logging.DEBUG, traceback.format_exc()) From a7dcd6d9e8adfe3124aa13db1b1536e6799d822c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 16:19:13 +1000 Subject: [PATCH 108/250] Minor: Add comment on TimeLinerInterface subclass filter --- volatility3/framework/plugins/timeliner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index 56fe465e49..d1cb9f4601 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -248,6 +248,7 @@ def run(self): # Identify plugins that we can run which output datetimes for plugin_class in self.usable_plugins: if not issubclass(plugin_class, TimeLinerInterface): + # get_usable_plugins() should filter this, but adding a safeguard just in case continue if filter_list and not any( From ebb9f0c20163fe215c72d510e9188dbf1d3c318a Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 17:05:45 +1000 Subject: [PATCH 109/250] Fix typo in timeliner function --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 9b6f05c4d0..ad2c3b1be3 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -342,7 +342,7 @@ def generate_timeline(self): description = f"Cached Inode for {inode_out.path}" yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time - yield description, timeliner.TimeLinerType.CHANGE, inode_out.change_time + yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time @staticmethod def format_fields_with_headers(headers, generator): From 7b2e259c6dca79b1d36acdc82bc8d7b76f7aa00b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 17:06:29 +1000 Subject: [PATCH 110/250] fix wrong variable names --- volatility3/framework/plugins/linux/pagecache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index ad2c3b1be3..81e1f36015 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -78,8 +78,8 @@ def to_user( cached_pages = int(self.inode.i_mapping.nrpages) file_mode = self.inode.get_file_mode() access_time_dt = self.inode.get_access_time() - modification_time_str = self.inode.get_modification_time() - change_time_str = self.inode.get_change_time() + modification_time_dt = self.inode.get_modification_time() + change_time_dt = self.inode.get_change_time() inode_user = InodeUser( superblock_addr=superblock_addr, @@ -92,8 +92,8 @@ def to_user( cached_pages=cached_pages, file_mode=file_mode, access_time=access_time_dt, - modification_time=modification_time_str, - change_time=change_time_str, + modification_time=modification_time_dt, + change_time=change_time_dt, path=self.path, ) return inode_user From 48ae43d64edd457b65eb40174fb00b54202aabda Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 12 Sep 2024 17:22:52 +1000 Subject: [PATCH 111/250] Bumping the major version since the output changed --- volatility3/framework/plugins/linux/check_creds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index 3e292ae33f..b7f73c3eb0 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -13,7 +13,7 @@ class Check_creds(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): From 867c8e3f1bdeb0fa57dd3e02056f32b526a81fb4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 15:33:19 +1000 Subject: [PATCH 112/250] Linux: Add support for threads in both lsof and sockstat plugins. - lsof plugin: source code refactored - lsof plugin: Added the 'device' column to complete the inode information. An inode number is specific to the filesystem/device it belongs to. - lsof/sockstat plugins: Add threads support. Threads may or may not share the file descriptor table with the thread group leader, depending on whether the CLONE_FILES flag is included in the clone() syscall. Also, once started, a thread can unshare the fd table with its parent. Refer to the unshare() libc syscall wrapper man page, unshare(2). Additionally, note that the Linux lsof command in user space includes thread listings by default as well. Now, there are two columns to identify the thread group ID (PID) and the task/thread ID (TID). - Added inode getters from both, the dentry and file structs. From kernels +3.9 the file struct cached the inode pointer. So, when possible, we get this value. - Improve smear protection in these plugins and various APIs (https://github.com/volatilityfoundation/volatility3/pull/1243) --- volatility3/framework/plugins/linux/lsof.py | 229 +++++++++++------- .../framework/plugins/linux/sockstat.py | 18 +- .../framework/symbols/linux/__init__.py | 4 +- .../symbols/linux/extensions/__init__.py | 42 +++- 4 files changed, 185 insertions(+), 108 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 360f897490..0325ebd4cb 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -1,12 +1,12 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""A module containing a collection of plugins that produce data typically -found in Linux's /proc file system.""" -import logging, datetime +import logging +from datetime import datetime +from dataclasses import dataclass, astuple, field from typing import List, Callable -from volatility3.framework import renderers, interfaces, constants, exceptions +from volatility3.framework import renderers, interfaces, constants from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -17,11 +17,94 @@ vollog = logging.getLogger(__name__) +@dataclass +class FDUser: + """FD user representation, featuring augmented information and formatted fields. + This is the data the plugin will eventually display. + """ + + task_tgid: int + task_tid: int + task_comm: str + fd_num: int + full_path: str + device: str = field(default=renderers.NotAvailableValue()) + inode_num: int = field(default=renderers.NotAvailableValue()) + inode_type: str = field(default=renderers.NotAvailableValue()) + file_mode: str = field(default=renderers.NotAvailableValue()) + change_time: datetime = field(default=renderers.NotAvailableValue()) + modification_time: datetime = field(default=renderers.NotAvailableValue()) + access_time: datetime = field(default=renderers.NotAvailableValue()) + inode_size: int = field(default=renderers.NotAvailableValue()) + + +@dataclass +class FDInternal: + """FD internal representation containing only the core objects + + Fields: + task: 'task_truct' object + fd_fields: FD fields as obtained from LinuxUtilities.files_descriptors_for_process() + """ + + task: interfaces.objects.ObjectInterface + fd_fields: tuple[int, int, str] + + def to_user(self) -> FDUser: + """Augment the FD information to be presented to the user + + Returns: + An InodeUser dataclass + """ + # Ensure all types are atomic immutable. Otherwise, astuple() will take a long + # time doing a deepcopy of the Volatility objects. + task_tgid = int(self.task.tgid) + task_tid = int(self.task.pid) + task_comm = utility.array_to_string(self.task.comm) + fd_num, filp, full_path = self.fd_fields + fd_num = int(fd_num) + full_path = str(full_path) + inode = filp.get_inode() + if inode: + superblock_ptr = inode.i_sb + if superblock_ptr and superblock_ptr.is_readable(): + device = f"{superblock_ptr.major}:{superblock_ptr.minor}" + else: + device = renderers.NotAvailableValue() + + fd_user = FDUser( + task_tgid=task_tgid, + task_tid=task_tid, + task_comm=task_comm, + fd_num=fd_num, + full_path=full_path, + device=device, + inode_num=int(inode.i_ino), + inode_type=inode.get_inode_type() or renderers.UnparsableValue(), + file_mode=inode.get_file_mode(), + change_time=inode.get_change_time(), + modification_time=inode.get_modification_time(), + access_time=inode.get_access_time(), + inode_size=int(inode.i_size), + ) + else: + # We use the dataclasses' default values + fd_user = FDUser( + task_tgid=task_tgid, + task_tid=task_tid, + task_comm=task_comm, + fd_num=fd_num, + full_path=full_path, + ) + + return fd_user + + class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (1, 2, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -45,126 +128,86 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ), ] - @classmethod - def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface): - try: - dentry = filp.get_dentry() - if dentry: - inode_object = dentry.d_inode - if inode_object and inode_object.is_valid(): - itype = ( - inode_object.get_inode_type() or renderers.NotAvailableValue() - ) - return ( - inode_object.i_ino, - itype, - inode_object.i_size, - inode_object.get_file_mode(), - inode_object.get_change_time(), - inode_object.get_modification_time(), - inode_object.get_access_time(), - ) - except (exceptions.InvalidAddressException, AttributeError) as e: - vollog.warning(f"Can't get inode metadata: {e}") - return None - @classmethod def list_fds( cls, context: interfaces.context.ContextInterface, - symbol_table: str, + vmlinux_module_name: str, filter_func: Callable[[int], bool] = lambda _: False, - ): + ) -> FDInternal: + """Enumerates open file descriptors in tasks + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + filter_func: A function which takes a process object and returns True if the process + should be ignored/filtered + + Yields: + A FDInternal object + """ linuxutils_symbol_table = None - for task in pslist.PsList.list_tasks(context, symbol_table, filter_func): + for task in pslist.PsList.list_tasks( + context, vmlinux_module_name, filter_func, include_threads=True + ): if linuxutils_symbol_table is None: if constants.BANG not in task.vol.type_name: raise ValueError("Task is not part of a symbol table") linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0] - task_comm = utility.array_to_string(task.comm) - pid = int(task.pid) - fd_generator = linux.LinuxUtilities.files_descriptors_for_process( context, linuxutils_symbol_table, task ) for fd_fields in fd_generator: - yield pid, task_comm, task, fd_fields + yield FDInternal(task=task, fd_fields=fd_fields) - @classmethod - def list_fds_and_inodes( - cls, - context: interfaces.context.ContextInterface, - symbol_table: str, - filter_func: Callable[[int], bool] = lambda _: False, - ): - for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds( - context, symbol_table, filter_func - ): - inode_metadata = cls.get_inode_metadata(filp) - if inode_metadata is None: - inode_metadata = tuple( - interfaces.renderers.BaseAbsentValue() for _ in range(7) - ) - yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata - - def _generator(self, pids, symbol_table): + def _generator(self, pids, vmlinux_module_name): filter_func = pslist.PsList.create_pid_filter(pids) - fds_generator = self.list_fds_and_inodes( - self.context, symbol_table, filter_func=filter_func - ) - - for ( - pid, - task_comm, - task, - fd_num, - filp, - full_path, - inode_metadata, - ) in fds_generator: - inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata - fields = ( - pid, - task_comm, - fd_num, - full_path, - inode_num, - itype, - imode, - ctime, - mtime, - atime, - file_size, - ) - yield (0, fields) + for fd_internal in self.list_fds( + self.context, vmlinux_module_name, filter_func=filter_func + ): + fd_user = fd_internal.to_user() + yield (0, astuple(fd_user)) def run(self): pids = self.config.get("pid", None) - symbol_table = self.config["kernel"] + vmlinux_module_name = self.config["kernel"] tree_grid_args = [ ("PID", int), + ("TID", int), ("Process", str), ("FD", int), ("Path", str), + ("Device", str), ("Inode", int), ("Type", str), ("Mode", str), - ("Changed", datetime.datetime), - ("Modified", datetime.datetime), - ("Accessed", datetime.datetime), + ("Changed", datetime), + ("Modified", datetime), + ("Accessed", datetime), ("Size", int), ] - return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) + return renderers.TreeGrid( + tree_grid_args, self._generator(pids, vmlinux_module_name) + ) def generate_timeline(self): pids = self.config.get("pid", None) - symbol_table = self.config["kernel"] - for row in self._generator(pids, symbol_table): - _depth, row_data = row - description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"' - yield description, timeliner.TimeLinerType.CHANGED, row_data[7] - yield description, timeliner.TimeLinerType.MODIFIED, row_data[8] - yield description, timeliner.TimeLinerType.ACCESSED, row_data[9] + vmlinux_module_name = self.config["kernel"] + + filter_func = pslist.PsList.create_pid_filter(pids) + for fd_internal in self.list_fds( + self.context, vmlinux_module_name, filter_func=filter_func + ): + fd_user = fd_internal.to_user() + + description = ( + f"Process {fd_user.task_comm} ({fd_user.task_tgid}/{fd_user.task_tid}) " + f"Open '{fd_user.full_path}'" + ) + + yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time + yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time + yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index b0503b1052..c76e0a6b49 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -22,9 +22,10 @@ class SockHandlers(interfaces.configuration.VersionableInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) - def __init__(self, vmlinux, task): + def __init__(self, vmlinux, task, *args, **kwargs): + super().__init__(*args, **kwargs) self._vmlinux = vmlinux self._task = task @@ -438,7 +439,7 @@ class Sockstat(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -452,7 +453,7 @@ def get_requirements(cls): name="SockHandlers", component=SockHandlers, version=(1, 0, 0) ), requirements.PluginRequirement( - name="lsof", plugin=lsof.Lsof, version=(1, 1, 0) + name="lsof", plugin=lsof.Lsof, version=(2, 0, 0) ), requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) @@ -507,8 +508,9 @@ def list_sockets( dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func) - for _pid, _task_comm, task, fd_fields in fd_generator: - fd_num, filp, _full_path = fd_fields + for fd_internal in fd_generator: + fd_num, filp, _full_path = fd_internal.fd_fields + task = fd_internal.task if filp.f_op not in (sfop_addr, dfop_addr): continue @@ -617,6 +619,7 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): fields = ( netns_id, + task.tgid, task.pid, fd_num, format_hints.Hex(sock.vol.offset), @@ -636,7 +639,8 @@ def run(self): tree_grid_args = [ ("NetNS", int), - ("Pid", int), + ("PID", int), + ("TID", int), ("FD", int), ("Sock Offset", format_hints.Hex), ("Family", str), diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 91abf7db4e..6e92975178 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -256,7 +256,7 @@ def files_descriptors_for_process( task: interfaces.objects.ObjectInterface, ): # task.files can be null - if not task.files: + if not (task.files and task.files.is_readable()): return None fd_table = task.files.get_fds() @@ -276,7 +276,7 @@ def files_descriptors_for_process( ) for fd_num, filp in enumerate(fds): - if filp != 0: + if filp and filp.is_readable(): full_path = LinuxUtilities.path_for_file(context, task, filp) yield fd_num, filp, full_path diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fdd34403ac..fd5ddcffae 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -843,25 +843,55 @@ def get_subdirs(self) -> interfaces.objects.ObjectInterface: dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry" yield from list_head_member.to_list(dentry_type_name, walk_member) + def get_inode(self) -> interfaces.objects.ObjectInterface: + """Returns the inode associated with this dentry""" + + inode_ptr = self.d_inode + if not (inode_ptr and inode_ptr.is_valid()): + return None + + return inode_ptr.dereference() + class struct_file(objects.StructType): def get_dentry(self) -> interfaces.objects.ObjectInterface: - if self.has_member("f_dentry"): - return self.f_dentry - elif self.has_member("f_path"): + """Returns a pointer to the dentry associated with this file""" + if self.has_member("f_path"): return self.f_path.dentry + elif self.has_member("f_dentry"): + return self.f_dentry else: raise AttributeError("Unable to find file -> dentry") def get_vfsmnt(self) -> interfaces.objects.ObjectInterface: """Returns the fs (vfsmount) where this file is mounted""" - if self.has_member("f_vfsmnt"): - return self.f_vfsmnt - elif self.has_member("f_path"): + if self.has_member("f_path"): return self.f_path.mnt + elif self.has_member("f_vfsmnt"): + return self.f_vfsmnt else: raise AttributeError("Unable to find file -> vfs mount") + def get_inode(self) -> interfaces.objects.ObjectInterface: + """Returns an inode associated with this file""" + + inode_ptr = None + if self.has_member("f_inode") and self.f_inode and self.f_inode.is_readable(): + # Try first the cached value, kernels +3.9 + inode_ptr = self.f_inode + + if not (inode_ptr and inode_ptr.is_valid()): + dentry_ptr = self.get_dentry() + if not (dentry_ptr and dentry_ptr.is_readable()): + return None + + inode_ptr = dentry_ptr.d_inode + + if not (inode_ptr and inode_ptr.is_valid()): + return None + + return inode_ptr.dereference() + class list_head(objects.StructType, collections.abc.Iterable): def to_list( From aecd31f0953142993dc10126be77441b8178dc51 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 16:09:49 +1000 Subject: [PATCH 113/250] Add missing pointer verification --- volatility3/framework/symbols/linux/extensions/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fd5ddcffae..46bed72aa2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -847,7 +847,7 @@ def get_inode(self) -> interfaces.objects.ObjectInterface: """Returns the inode associated with this dentry""" inode_ptr = self.d_inode - if not (inode_ptr and inode_ptr.is_valid()): + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): return None return inode_ptr.dereference() @@ -880,14 +880,14 @@ def get_inode(self) -> interfaces.objects.ObjectInterface: # Try first the cached value, kernels +3.9 inode_ptr = self.f_inode - if not (inode_ptr and inode_ptr.is_valid()): + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): dentry_ptr = self.get_dentry() if not (dentry_ptr and dentry_ptr.is_readable()): return None inode_ptr = dentry_ptr.d_inode - if not (inode_ptr and inode_ptr.is_valid()): + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): return None return inode_ptr.dereference() From 32eaeeb7d2b631b4ff9c89a2bea2793e2ca36135 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 16:13:00 +1000 Subject: [PATCH 114/250] Fix typing typo --- volatility3/framework/plugins/linux/lsof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 0325ebd4cb..527815738f 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -4,7 +4,7 @@ import logging from datetime import datetime from dataclasses import dataclass, astuple, field -from typing import List, Callable +from typing import List, Callable, Tuple from volatility3.framework import renderers, interfaces, constants from volatility3.framework.configuration import requirements @@ -48,7 +48,7 @@ class FDInternal: """ task: interfaces.objects.ObjectInterface - fd_fields: tuple[int, int, str] + fd_fields: Tuple[int, int, str] def to_user(self) -> FDUser: """Augment the FD information to be presented to the user From e084086ee5f2d9dad29182bc4616912cbfe53f1e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 16:18:26 +1000 Subject: [PATCH 115/250] Improve pointer verification in super_block->get_type() --- .../symbols/linux/extensions/__init__.py | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fdd34403ac..aa18b8262a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -638,11 +638,23 @@ def get_flags_opts(self) -> Iterable[str]: ] return sb_opts - def get_type(self): - mnt_sb_type = utility.pointer_to_string(self.s_type.name, count=255) - if self.s_subtype: - mnt_sb_subtype = utility.pointer_to_string(self.s_subtype, count=255) + def get_type(self) -> Optional[str]: + """Gets the superblock filesystem type string""" + + s_type_ptr = self.s_type + if not (s_type_ptr and s_type_ptr.is_readable()): + return None + + s_type_name_ptr = s_type_ptr.name + if not (s_type_name_ptr and s_type_name_ptr.is_readable()): + return None + + mnt_sb_type = utility.pointer_to_string(s_type_name_ptr, count=255) + s_subtype_ptr = self.s_subtype + if s_subtype_ptr and s_subtype_ptr.is_readable(): + mnt_sb_subtype = utility.pointer_to_string(s_subtype_ptr, count=255) mnt_sb_type += "." + mnt_sb_subtype + return mnt_sb_type @@ -843,25 +855,55 @@ def get_subdirs(self) -> interfaces.objects.ObjectInterface: dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry" yield from list_head_member.to_list(dentry_type_name, walk_member) + def get_inode(self) -> interfaces.objects.ObjectInterface: + """Returns the inode associated with this dentry""" + + inode_ptr = self.d_inode + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): + return None + + return inode_ptr.dereference() + class struct_file(objects.StructType): def get_dentry(self) -> interfaces.objects.ObjectInterface: - if self.has_member("f_dentry"): - return self.f_dentry - elif self.has_member("f_path"): + """Returns a pointer to the dentry associated with this file""" + if self.has_member("f_path"): return self.f_path.dentry + elif self.has_member("f_dentry"): + return self.f_dentry else: raise AttributeError("Unable to find file -> dentry") def get_vfsmnt(self) -> interfaces.objects.ObjectInterface: """Returns the fs (vfsmount) where this file is mounted""" - if self.has_member("f_vfsmnt"): - return self.f_vfsmnt - elif self.has_member("f_path"): + if self.has_member("f_path"): return self.f_path.mnt + elif self.has_member("f_vfsmnt"): + return self.f_vfsmnt else: raise AttributeError("Unable to find file -> vfs mount") + def get_inode(self) -> interfaces.objects.ObjectInterface: + """Returns an inode associated with this file""" + + inode_ptr = None + if self.has_member("f_inode") and self.f_inode and self.f_inode.is_readable(): + # Try first the cached value, kernels +3.9 + inode_ptr = self.f_inode + + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): + dentry_ptr = self.get_dentry() + if not (dentry_ptr and dentry_ptr.is_readable()): + return None + + inode_ptr = dentry_ptr.d_inode + + if not (inode_ptr and inode_ptr.is_readable() and inode_ptr.is_valid()): + return None + + return inode_ptr.dereference() + class list_head(objects.StructType, collections.abc.Iterable): def to_list( From 997abeda6d9014a028f6c2b7a9e11352320c142f Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 17:27:02 +1000 Subject: [PATCH 116/250] Linux lsof: Add namespace dentry name --- .../framework/symbols/linux/__init__.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 91abf7db4e..57b45667ec 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -169,13 +169,30 @@ def _get_new_sock_pipe_path(cls, context, task, filp) -> str: Returns: str: Sock pipe pathname relative to the task's root directory. """ + # FIXME: This function must be moved to the 'dentry' object extension + # Also, the scope of this function went beyond the sock pipe path, so we need to rename this. + # Once https://github.com/volatilityfoundation/volatility3/pull/1263 is merged, replace the + # dentry inode getters + + if not (filp and filp.is_readable()): + return f" {filp:x}" + dentry = filp.get_dentry() + if not (dentry and dentry.is_readable()): + return f" {dentry:x}" kernel_module = cls.get_module_from_volobj_type(context, dentry) sym_addr = dentry.d_op.d_dname + if not (sym_addr and sym_addr.is_readable()): + return f" {sym_addr:x}" + symbs = list(kernel_module.get_symbols_by_absolute_location(sym_addr)) + inode = dentry.d_inode + if not (inode and inode.is_readable() and inode.is_valid()): + return f" {inode:x}" + if len(symbs) == 1: sym = symbs[0].split(constants.BANG)[1] @@ -191,13 +208,36 @@ def _get_new_sock_pipe_path(cls, context, task, filp) -> str: elif sym == "simple_dname": pre_name = cls._get_path_file(task, filp) + elif sym == "ns_dname": + # From Kernels 3.19 + + # In Kernels >= 6.9, see Linux kernel commit 1fa08aece42512be072351f482096d5796edf7ca + # ns_common->stashed change from 'atomic64_t' to 'dentry*' + try: + ns_common_type = kernel_module.get_type("ns_common") + stashed_template = ns_common_type.child_template("stashed") + stashed_type_full_name = stashed_template.vol.type_name + stashed_type_name = stashed_type_full_name.split(constants.BANG)[-1] + if stashed_type_name == "atomic64_t": + # 3.19 <= Kernels < 6.9 + ns_ops = dentry.d_fsdata.dereference().cast( + "proc_ns_operations" + ) + else: + # Kernels >= 6.9 + ns_common = inode.i_private.dereference().cast("ns_common") + ns_ops = ns_common.ops + + pre_name = utility.pointer_to_string(ns_ops.name, 255) + except IndexError: + ret = "" else: - pre_name = f"" + pre_name = f" {sym}" - ret = f"{pre_name}:[{dentry.d_inode.i_ino:d}]" + ret = f"{pre_name}:[{inode.i_ino:d}]" else: - ret = f" {sym_addr:x}" + ret = f" {sym_addr:x}" return ret From cd2af74e6d0c554e81d1e67a8020195cfca59983 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 13 Sep 2024 17:58:21 +1000 Subject: [PATCH 117/250] Improve pointers address verification and return message chain --- .../framework/symbols/linux/__init__.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 57b45667ec..2410d2627b 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -217,29 +217,32 @@ def _get_new_sock_pipe_path(cls, context, task, filp) -> str: ns_common_type = kernel_module.get_type("ns_common") stashed_template = ns_common_type.child_template("stashed") stashed_type_full_name = stashed_template.vol.type_name - stashed_type_name = stashed_type_full_name.split(constants.BANG)[-1] + stashed_type_name = stashed_type_full_name.split(constants.BANG)[1] if stashed_type_name == "atomic64_t": # 3.19 <= Kernels < 6.9 - ns_ops = dentry.d_fsdata.dereference().cast( - "proc_ns_operations" - ) + fsdata_ptr = dentry.d_fsdata + if not (fsdata_ptr and fsdata_ptr.is_readable()): + raise IndexError + + ns_ops = fsdata_ptr.dereference().cast("proc_ns_operations") else: # Kernels >= 6.9 - ns_common = inode.i_private.dereference().cast("ns_common") + private_ptr = inode.i_private + if not (private_ptr and private_ptr.is_readable()): + raise IndexError + + ns_common = private_ptr.dereference().cast("ns_common") ns_ops = ns_common.ops pre_name = utility.pointer_to_string(ns_ops.name, 255) except IndexError: - ret = "" + pre_name = "" else: pre_name = f" {sym}" - - ret = f"{pre_name}:[{inode.i_ino:d}]" - else: - ret = f" {sym_addr:x}" + pre_name = f" {sym_addr:x}" - return ret + return f"{pre_name}:[{inode.i_ino:d}]" @classmethod def path_for_file(cls, context, task, filp) -> str: From 67ee382c3229f10d4e29958b6a5bf257e29ed8f2 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 13 Sep 2024 18:53:33 +0200 Subject: [PATCH 118/250] use default req value in config_value call --- volatility3/framework/interfaces/configuration.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/interfaces/configuration.py b/volatility3/framework/interfaces/configuration.py index 3bb3cb0193..da0a4556c6 100644 --- a/volatility3/framework/interfaces/configuration.py +++ b/volatility3/framework/interfaces/configuration.py @@ -494,8 +494,7 @@ def unsatisfied( """Validates the instance requirement based upon its `instance_type`.""" config_path = path_join(config_path, self.name) - - value = self.config_value(context, config_path, None) + value = self.config_value(context, config_path, self.default) if not isinstance(value, self.instance_type): vollog.log( constants.LOGLEVEL_V, @@ -536,7 +535,7 @@ def unsatisfied( """Checks to see if a class can be recovered.""" config_path = path_join(config_path, self.name) - value = self.config_value(context, config_path, None) + value = self.config_value(context, config_path, self.default) self._cls = None if value is not None and isinstance(value, str): if "." in value: From ba0c975e73207ee6555bd80067460ddbea6426a2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Fri, 13 Sep 2024 18:50:47 -0500 Subject: [PATCH 119/250] Address all feedback --- .../framework/plugins/windows/pe_symbols.py | 128 +++++++++--------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 30e9b49d16..85bfb572e3 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -1,11 +1,12 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +import copy import io import logging import ntpath -from typing import Dict, Tuple, Optional, List, Generator, Union +from typing import Dict, Tuple, Optional, List, Generator, Union, Callable import pefile @@ -29,11 +30,13 @@ # how wanted modules/symbols are specified, such as: # {"ntdll.dll" : {wanted_addresses : [42, 43, 43]}} # {"ntdll.dll" : {wanted_names : ["NtCreateThread"]}} -filter_modules_type = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] +filter_module_info = Union[Dict[str, List[str]], Dict[str, List[int]]] +filter_modules_type = Dict[str, filter_module_info] # holds resolved symbols # {"ntdll.dll": [("Bob", 123), ("Alice", 456)]} -found_symbols_type = Dict[str, List[Tuple[str, int]]] +found_symbols_module = List[Tuple[str, int]] +found_symbols_type = Dict[str, found_symbols_module] # used to hold informatin about a range (VAD or kernel module) # (start address, size, file path) @@ -61,7 +64,8 @@ class PESymbolFinder: cached_int_dict = Dict[str, Optional[int]] cached_value = Union[int, str, None] - cached_value_dict = Dict[str, Union[Dict[str, List[str]], Dict[str, List[int]]]] + cached_module_lists = Union[Dict[str, List[str]], Dict[str, List[int]]] + cached_value_dict = Dict[str, cached_module_lists] def __init__( self, @@ -402,11 +406,11 @@ def addresses_for_process_symbols( context, layer_name, symbol_table_name, symbols ) - found_symbols = PESymbols.find_symbols( + found_symbols, missing_symbols = PESymbols.find_symbols( context, config_path, symbols, collected_modules ) - for mod_name, unresolved_symbols in symbols.items(): + for mod_name, unresolved_symbols in missing_symbols.items(): for symbol in unresolved_symbols: vollog.debug(f"Unable to resolve symbol {symbol} in module {mod_name}") @@ -449,7 +453,7 @@ def path_and_symbol_for_address( filename: {wanted_addresses_identifier: [address]} } - found_symbols = PESymbols.find_symbols( + found_symbols, _missing_msybols = PESymbols.find_symbols( context, config_path, filter_module, collected_modules ) @@ -624,61 +628,46 @@ def _find_symbols_through_exports( @staticmethod def _get_symbol_value( - wanted_modules: PESymbolFinder.cached_value_dict, - mod_name: str, + wanted_symbols: filter_module_info, symbol_resolver: PESymbolFinder, - ) -> Generator[Tuple[str, int], None, None]: + ) -> Generator[Tuple[str, int, str, int], None, None]: """ Enumerates the symbols specified as wanted by the calling plugin - removes entries from wanted_modules as they found to avoid PDB or export analysis after resolving all symbols - Args: - wanted_modules: the dictionary of modules and symbols to resolve. Modified to remove symbols as they are resolved. - mod_name: the name of module to resolve symbols in + wanted_symbols: the set of symbols for a particular module + symbol_resolver: method in a layer to resolve the symbols Returns: - Tuple[str, int]: the name and address of resolved symbols + Tuple[str, int, str, int]: the index and value of the found symbol in the wanted list, and the name and address of resolved symbol """ - wanted_symbols = wanted_modules[mod_name] - if ( wanted_names_identifier not in wanted_symbols and wanted_addresses_identifier not in wanted_symbols ): vollog.warning( - f"Invalid `wanted_symbols` sent to `find_symbols` for module {mod_name}. addresses and names keys both misssing." + f"Invalid `wanted_symbols` sent to `find_symbols`. addresses and names keys both misssing." ) return - symbol_keys = [ - (wanted_names_identifier, "get_address_for_name"), - (wanted_addresses_identifier, "get_name_for_address"), + symbol_keys: List[Tuple[str, Callable]] = [ + (wanted_names_identifier, symbol_resolver.get_address_for_name), + (wanted_addresses_identifier, symbol_resolver.get_name_for_address), ] for symbol_key, symbol_getter in symbol_keys: # address or name if symbol_key in wanted_symbols: # walk each wanted address or name - for wanted_value in wanted_symbols[symbol_key]: - symbol_value = symbol_resolver.__getattribute__(symbol_getter)( - wanted_value - ) + for value_index, wanted_value in enumerate(wanted_symbols[symbol_key]): + symbol_value = symbol_getter(wanted_value) + if symbol_value: - # yield out symbol name, symbol address + # yield out deleteion key, deletion index, symbol name, symbol address if symbol_key == wanted_names_identifier: - yield wanted_value, symbol_value # type: ignore + yield symbol_key, value_index, wanted_value, symbol_value # type: ignore else: - yield symbol_value, wanted_value # type: ignore - - index = wanted_modules[mod_name][symbol_key].index(wanted_value) # type: ignore - - del wanted_modules[mod_name][symbol_key][index] - - # if all names or addresses from a module are found, delete the key - if not wanted_modules[mod_name][symbol_key]: - del wanted_modules[mod_name][symbol_key] - break + yield symbol_key, value_index, symbol_value, wanted_value # type: ignore @staticmethod def _resolve_symbols_through_methods( @@ -687,7 +676,7 @@ def _resolve_symbols_through_methods( module_instances: collected_modules_info, wanted_modules: PESymbolFinder.cached_value_dict, mod_name: str, - ) -> Generator[Tuple[str, int], None, None]: + ) -> Tuple[found_symbols_module, PESymbolFinder.cached_module_lists]: """ Attempts to resolve every wanted symbol in `mod_name` Every layer is enumerated for maximum chance of recovery @@ -697,35 +686,53 @@ def _resolve_symbols_through_methods( wanted_modules: The symbols to resolve tied to their module names mod_name: name of the module to resolve symbols in Returns: - Generator[Tuple[str, int]]: resolved symbol names and addresses + Tuple[found_symbols_module, PESymbolFinder.cached_module_lists]: The set of found symbols and the ones that could not be resolved """ symbol_resolving_methods = [ PESymbols._find_symbols_through_pdb, PESymbols._find_symbols_through_exports, ] + found: found_symbols_module = [] + + # the symbols wanted from this module by the caller + wanted = wanted_modules[mod_name] + + # make a copy to remove from inside this function for returning to the caller + remaining = copy.deepcopy(wanted) + for method in symbol_resolving_methods: + # every layer where this module was found through the given method for symbol_resolver in method( context, config_path, module_instances, mod_name ): vollog.debug(f"Have resolver for method {method}") - yield from PESymbols._get_symbol_value( - wanted_modules, mod_name, symbol_resolver - ) - - if not wanted_modules[mod_name]: + for ( + symbol_key, + value_index, + symbol_name, + symbol_address, + ) in PESymbols._get_symbol_value(remaining, symbol_resolver): + found.append((symbol_name, symbol_address)) + del remaining[symbol_key][value_index] + + # everything was resolved, stop this resolver + if not remaining: break - if not wanted_modules[mod_name]: + # stop all resolving + if not remaining: break + return found, remaining + @staticmethod def find_symbols( context: interfaces.context.ContextInterface, config_path: str, wanted_modules: PESymbolFinder.cached_value_dict, collected_modules: collected_modules_type, - ) -> found_symbols_type: + ) -> Tuple[found_symbols_type, PESymbolFinder.cached_value_dict]: """ Loops through each method of symbol analysis until each wanted symbol is found Returns the resolved symbols as a dictionary that includes the name and runtime address @@ -734,9 +741,10 @@ def find_symbols( wanted_modules: the dictionary of modules and symbols to resolve. Modified to remove symbols as they are resolved. collected_modules: return value from `get_kernel_modules` or `get_process_modules` Returns: - found_symbols_type: The set of symbols resolved to their name and/or address + Tuple[found_symbols_type, PESymbolFinder.cached_value_dict]: The set of found symbols but the ones that could not be resolved """ found_symbols: found_symbols_type = {} + missing_symbols: PESymbolFinder.cached_value_dict = {} for mod_name in wanted_modules: if mod_name not in collected_modules: @@ -745,24 +753,20 @@ def find_symbols( module_instances = collected_modules[mod_name] # try to resolve the symbols for `mod_name` through each method (PDB and export table currently) - for symbol_name, address in PESymbols._resolve_symbols_through_methods( + ( + found_in_module, + missing_in_module, + ) = PESymbols._resolve_symbols_through_methods( context, config_path, module_instances, wanted_modules, mod_name - ): - if mod_name not in found_symbols: - found_symbols[mod_name] = [] + ) - found_symbols[mod_name].append((symbol_name, address)) + if found_in_module: + found_symbols[mod_name] = found_in_module - # stop processing the layers (processes) if we found all the symbols for this module - if not wanted_modules[mod_name]: - break + if missing_in_module: + missing_symbols[mod_name] = missing_in_module - # stop processing this module if/when all symbols are found - if not wanted_modules[mod_name]: - del wanted_modules[mod_name] - break - - return found_symbols + return found_symbols, missing_symbols @staticmethod def get_kernel_modules( @@ -986,7 +990,7 @@ def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: self.context, kernel.layer_name, kernel.symbol_table_name, filter_module ) - found_symbols = PESymbols.find_symbols( + found_symbols, _missing_symbols = PESymbols.find_symbols( self.context, self.config_path, filter_module, collected_modules ) From 21d21cf4b8ec3aafa5e98c563b207b21c41d5c3f Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 14 Sep 2024 16:26:38 -0500 Subject: [PATCH 120/250] Break properly in all paths. Help callers to ensure always lower case module name matching. --- volatility3/framework/plugins/windows/pe_symbols.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 85bfb572e3..d0ddcac57b 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -380,7 +380,7 @@ def filename_for_path(filepath: str) -> str: Returns: str: the bsae file name of the full path """ - return ntpath.basename(filepath) + return ntpath.basename(filepath).lower() @staticmethod def addresses_for_process_symbols( @@ -717,11 +717,12 @@ def _resolve_symbols_through_methods( del remaining[symbol_key][value_index] # everything was resolved, stop this resolver - if not remaining: + if not remaining[symbol_key]: break # stop all resolving - if not remaining: + if not remaining[symbol_key]: + del remaining[symbol_key] break return found, remaining From 291bc878ad771388f3514b2c66d31b7325b2bc01 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 14 Sep 2024 16:31:33 -0500 Subject: [PATCH 121/250] Break in a cleaner flow --- volatility3/framework/plugins/windows/pe_symbols.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index d0ddcac57b..44254513f3 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -701,6 +701,8 @@ def _resolve_symbols_through_methods( # make a copy to remove from inside this function for returning to the caller remaining = copy.deepcopy(wanted) + done_processing = False + for method in symbol_resolving_methods: # every layer where this module was found through the given method for symbol_resolver in method( @@ -717,12 +719,14 @@ def _resolve_symbols_through_methods( del remaining[symbol_key][value_index] # everything was resolved, stop this resolver + # remove this key from the remaining symbols to resolve if not remaining[symbol_key]: + del remaining[symbol_key] + done_processing = True break # stop all resolving - if not remaining[symbol_key]: - del remaining[symbol_key] + if done_processing: break return found, remaining From 322f79fb5040e3e7ebdfa9de8c82a639729a39f2 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sat, 14 Sep 2024 17:18:26 -0500 Subject: [PATCH 122/250] Bail as early as possible --- .../framework/plugins/windows/pe_symbols.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 44254513f3..0bf03e7d63 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -437,7 +437,6 @@ def path_and_symbol_for_address( Returns: Tuple[str|renderers.NotApplicableValue|renderers.NotAvailableValue, str|renderers.NotApplicableValue|renderers.NotAvailableValue] """ - if not address: return renderers.NotApplicableValue(), renderers.NotApplicableValue() @@ -458,7 +457,7 @@ def path_and_symbol_for_address( ) if not found_symbols or filename not in found_symbols: - return renderers.NotAvailableValue(), renderers.NotAvailableValue() + return filepath, renderers.NotAvailableValue() return filepath, found_symbols[filename][0][0] @@ -718,11 +717,14 @@ def _resolve_symbols_through_methods( found.append((symbol_name, symbol_address)) del remaining[symbol_key][value_index] - # everything was resolved, stop this resolver - # remove this key from the remaining symbols to resolve - if not remaining[symbol_key]: - del remaining[symbol_key] - done_processing = True + # everything was resolved, stop this resolver + # remove this key from the remaining symbols to resolve + if not remaining[symbol_key]: + del remaining[symbol_key] + done_processing = True + break + + if done_processing: break # stop all resolving @@ -885,6 +887,7 @@ def get_proc_vads_with_file_paths( for vad in vad_root.traverse(): filepath = vad.get_file_name() + if not isinstance(filepath, str) or filepath.count("\\") == 0: continue From 79b8ff7d05b56316d90be682f56db22401f2c265 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sun, 15 Sep 2024 18:29:05 -0500 Subject: [PATCH 123/250] Address final feedback --- .../plugins/windows/debugregisters.py | 23 ++++++++----------- .../framework/plugins/windows/pe_symbols.py | 10 ++++---- .../plugins/windows/unhooked_system_calls.py | 8 +++++++ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 65b2e625b9..945ba1df0e 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -148,12 +148,7 @@ def _generator( file3, sym3 = path_and_symbol(vads, dr3) # if none map to an actual file VAD then bail - if not ( - isinstance(file0, str) - or isinstance(file1, str) - or isinstance(file2, str) - or isinstance(file3, str) - ): + if not (file0 or file1 or file2 or file3): continue process_name = owner_proc.ImageFileName.cast( @@ -173,17 +168,17 @@ def _generator( thread.Tcb.State, dr7, format_hints.Hex(dr0), - file0, - sym0, + file0 or renderers.NotApplicableValue(), + sym0 or renderers.NotApplicableValue(), format_hints.Hex(dr1), - file1, - sym1, + file1 or renderers.NotApplicableValue(), + sym1 or renderers.NotApplicableValue(), format_hints.Hex(dr2), - file2, - sym2, + file2 or renderers.NotApplicableValue(), + sym2 or renderers.NotApplicableValue(), format_hints.Hex(dr3), - file3, - sym3, + file3 or renderers.NotApplicableValue(), + sym3 or renderers.NotApplicableValue(), ), ) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 0bf03e7d63..955098d6bf 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -423,7 +423,7 @@ def path_and_symbol_for_address( collected_modules: collected_modules_type, ranges: ranges_type, address: int, - ) -> Tuple[str, str]: + ) -> Tuple[Optional[str], Optional[str]]: """ Method for plugins to determine the file path and symbol name for a given address @@ -438,12 +438,12 @@ def path_and_symbol_for_address( Tuple[str|renderers.NotApplicableValue|renderers.NotAvailableValue, str|renderers.NotApplicableValue|renderers.NotAvailableValue] """ if not address: - return renderers.NotApplicableValue(), renderers.NotApplicableValue() + return None, None filepath = PESymbols.filepath_for_address(ranges, address) if not filepath: - return renderers.NotAvailableValue(), renderers.NotAvailableValue() + return None, None filename = PESymbols.filename_for_path(filepath).lower() @@ -452,12 +452,12 @@ def path_and_symbol_for_address( filename: {wanted_addresses_identifier: [address]} } - found_symbols, _missing_msybols = PESymbols.find_symbols( + found_symbols, _missing_symbols = PESymbols.find_symbols( context, config_path, filter_module, collected_modules ) if not found_symbols or filename not in found_symbols: - return filepath, renderers.NotAvailableValue() + return filepath, None return filepath, found_symbols[filename][0][0] diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 68f4c4b80a..c3d98254d1 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -71,6 +71,13 @@ class unhooked_system_calls(interfaces.plugins.PluginInterface): } } + # This data structure is used to track unique implementations of functions across processes + # The outer dictionary holds the module name (e.g., ntdll.dll) + # The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module + # The innermost dictionary holds the unique implementation (bytes) of a function across processes + # Each implementation is tracked along with the process(es) that host it + # For systems without malware, all functions should have the same implementation + # When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] @classmethod @@ -127,6 +134,7 @@ def _gather_code_bytes( except exceptions.InvalidAddressException: continue + # see the definition of _code_bytes_type for details of this data structure if dll_name not in code_bytes: code_bytes[dll_name] = {} From b788733683256e2cfd436750f616054f707252e9 Mon Sep 17 00:00:00 2001 From: Andrew Case Date: Sun, 15 Sep 2024 19:48:55 -0500 Subject: [PATCH 124/250] More comments on unhooked system calls --- .../plugins/windows/debugregisters.py | 4 ++++ .../plugins/windows/unhooked_system_calls.py | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/debugregisters.py b/volatility3/framework/plugins/windows/debugregisters.py index 945ba1df0e..57dd1822c9 100644 --- a/volatility3/framework/plugins/windows/debugregisters.py +++ b/volatility3/framework/plugins/windows/debugregisters.py @@ -1,6 +1,10 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + import logging from typing import Tuple, Optional, Generator, List, Dict diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index c3d98254d1..1a1e599407 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -1,6 +1,10 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + import logging from typing import Dict, Tuple, List, Generator @@ -162,20 +166,30 @@ def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: # code_bytes[dll_name][func_name][func_bytes] code_bytes = self._gather_code_bytes(kernel, found_symbols) + # walk the functions that were evaluated for functions in code_bytes.values(): + # cbb is the distinct groups of bytes (instructions) + # for this function across processes for func_name, cbb in functions.items(): + # the dict key here is the raw instructions, which is not helpful to look at + # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) cb = list(cbb.values()) - # same implementation in all + # if all processes map to the same implementation, then no malware is present if len(cb) == 1: yield 0, (func_name, "", len(cb[0])) else: - # find the processes that are hooked for reporting + # if there are differing implementations then it means + # that malware has overwritten system call(s) in infected processes + # max_idx and small_idx find which implementation of a system call has the least processes + # as all observed malware and open source projects only infected a few targets, leaving the + # rest with the original EDR hooks in place max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 small_idx = (~max_idx) & 1 ps = [] + # gather processes on small_idx since these are the malware infected ones for pid, pname in cb[small_idx]: ps.append("{:d}:{}".format(pid, pname)) From 10ac21da2cbca02d47dc9aa938c2fe0d560af23d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 16 Sep 2024 12:16:43 +0100 Subject: [PATCH 125/250] Windows: Remove the unnecessary requirement on verinfo Fixes #1267 --- volatility3/framework/plugins/windows/verinfo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 5b3c52bf6a..57b8dcd3fb 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -48,9 +48,6 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="modules", plugin=modules.Modules, version=(2, 0, 0) ), - requirements.VersionRequirement( - name="dlllist", component=dlllist.DllList, version=(2, 0, 0) - ), requirements.BooleanRequirement( name="extensive", description="Search physical layer for version information", From f77003b670be71e012d0caa52f936a5adba8f162 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 18 Sep 2024 12:50:58 +1000 Subject: [PATCH 126/250] Fix changes introduced to volatility3.framework.constants in PRs #838 and #1247 --- volatility3/framework/constants/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 8743a64b0b..27fae4ba1f 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -134,4 +134,5 @@ def __getattr__(name): ]: warnings.warn(f"{name} is deprecated", FutureWarning) return globals()[f"{deprecated_tag}{name}"] - return None + + return getattr(__import__(__name__), name) From 09fa859a92878b5d035e0dd5c44c0521661630fc Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 25 Sep 2024 09:02:51 +0100 Subject: [PATCH 127/250] Windows: change warnings around large memory maps to debug level as per issue #1256 --- volatility3/framework/plugins/windows/vadyarascan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 7bc3377c38..efcc70d07f 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -18,7 +18,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" _required_framework_version = (2, 4, 0) - _version = (1, 1, 0) + _version = (1, 1, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -68,7 +68,7 @@ def _generator(self): layer = self.context.layers[layer_name] for start, size in self.get_vad_maps(task): if size > sanity_check: - vollog.warn( + vollog.debug( f"VAD at 0x{start:x} over sanity-check size, not scanning" ) continue From 7f37135739c9ff951e73680f7cdf4333b47ee231 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Wed, 25 Sep 2024 13:13:33 -0500 Subject: [PATCH 128/250] Linux: Update sockstat to render process names Currently, process names are not displayed for sockets in the sockstat plugin, making analysis more painful than it needs to be. This updates the `list_sockets` classmethod and the `generator` method to return the process name in addition to the PID. Because this is changing the public interface, this commit includes a major version bump for `linux.sockstat.Sockstat`. --- volatility3/framework/plugins/linux/sockstat.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index b0503b1052..d3efc78dd8 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -22,7 +22,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) def __init__(self, vmlinux, task): self._vmlinux = vmlinux @@ -507,7 +507,7 @@ def list_sockets( dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func) - for _pid, _task_comm, task, fd_fields in fd_generator: + for _pid, task_comm, task, fd_fields in fd_generator: fd_num, filp, _full_path = fd_fields if filp.f_op not in (sfop_addr, dfop_addr): @@ -548,7 +548,7 @@ def list_sockets( except AttributeError: netns_id = NotAvailableValue() - yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields + yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields def _format_fields(self, sock_stat, protocol): """Prepare the socket fields to be rendered @@ -595,6 +595,7 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): ) for ( + task_comm, task, netns_id, fd_num, @@ -617,6 +618,7 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): fields = ( netns_id, + task_comm, task.pid, fd_num, format_hints.Hex(sock.vol.offset), @@ -636,6 +638,7 @@ def run(self): tree_grid_args = [ ("NetNS", int), + ("Process Name", str), ("Pid", int), ("FD", int), ("Sock Offset", format_hints.Hex), From 48d4048b487b665051c8f21a24ff8e77333ee682 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Fri, 27 Sep 2024 10:02:36 -0500 Subject: [PATCH 129/250] #816 - fixes and additional windows versions --- .../framework/plugins/windows/consoles.py | 287 ++++--- .../framework/plugins/windows/verinfo.py | 2 +- .../consoles-win10-17763-3232-x64.json | 71 +- .../consoles/consoles-win10-17763-x64.json | 71 +- .../consoles/consoles-win10-18362-x64.json | 73 +- .../consoles/consoles-win10-19041-x64.json | 73 +- .../consoles-win10-20348-1970-x64.json | 108 ++- .../consoles-win10-20348-2461-x64.json | 108 ++- .../consoles/consoles-win10-20348-x64.json | 108 ++- .../consoles/consoles-win10-22000-x64.json | 722 +++++++++++++++++ .../consoles-win10-22621-3672-x64.json | 722 +++++++++++++++++ .../consoles/consoles-win10-22621-x64.json | 722 +++++++++++++++++ .../consoles/consoles-win10-25398-x64.json | 723 ++++++++++++++++++ .../symbols/windows/extensions/consoles.py | 112 ++- 14 files changed, 3772 insertions(+), 130 deletions(-) create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json create mode 100644 volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index c9e8522a49..46699aa31f 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -11,12 +11,13 @@ from volatility3.framework import interfaces, symbols, exceptions from volatility3.framework import renderers +from volatility3.framework.interfaces import configuration from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows import pdbutil, versions +from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe, consoles from volatility3.plugins.windows import pslist, vadinfo, info, verinfo from volatility3.plugins.windows.registry import hivelist @@ -49,6 +50,9 @@ def get_requirements(cls): requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) + ), requirements.VersionRequirement( name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) ), @@ -92,18 +96,19 @@ def find_conhost_proc( """ for proc in proc_list: - try: - proc_id = proc.UniqueProcessId - proc_layer_name = proc.add_process_layer() + if utility.array_to_string(proc.ImageFileName).lower() == "conhost.exe": + try: + proc_id = proc.UniqueProcessId + proc_layer_name = proc.add_process_layer() - yield proc, proc_layer_name + yield proc, proc_layer_name - except exceptions.InvalidAddressException as excp: - vollog.debug( - "Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name + except exceptions.InvalidAddressException as excp: + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) ) - ) @classmethod def find_conhostexe( @@ -122,7 +127,6 @@ def find_conhostexe( """ for vad in conhost_proc.get_vad_root().traverse(): filename = vad.get_file_name() - if isinstance(filename, str) and filename.lower().endswith("conhost.exe"): base = vad.get_start() return base, vad.get_size() @@ -135,6 +139,9 @@ def determine_conhost_version( context: interfaces.context.ContextInterface, layer_name: str, nt_symbol_table: str, + config_path: str, + conhost_layer_name: str, + conhost_base: int, ) -> Tuple[str, Type]: """Tries to determine which symbol filename to use for the image's console information. This is similar to the netstat plugin. @@ -143,6 +150,9 @@ def determine_conhost_version( context: The context to retrieve required elements (layers, symbol tables) from layer_name: The name of the layer on which to operate nt_symbol_table: The name of the table containing the kernel symbols + config_path: The config path where to find symbol files + conhost_layer_name: The name of the conhot process memory layer + conhost_base: the base address of conhost.exe Returns: The filename of the symbol table to use and the associated class types. @@ -150,10 +160,6 @@ def determine_conhost_version( is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) - is_18363_or_later = versions.is_win10_18363_or_later( - context=context, symbol_table=nt_symbol_table - ) - if is_64bit: arch = "x64" else: @@ -211,23 +217,15 @@ def determine_conhost_version( (10, 0, 20348, 1970): "consoles-win10-20348-1970-x64", (10, 0, 20348, 2461): "consoles-win10-20348-2461-x64", (10, 0, 20348, 2520): "consoles-win10-20348-2461-x64", + (10, 0, 22000, 0): "consoles-win10-22000-x64", + (10, 0, 22621, 1): "consoles-win10-22621-x64", + (10, 0, 22621, 3672): "consoles-win10-22621-3672-x64", + (10, 0, 25398, 0): "consoles-win10-22000-x64", } # we do not need to check for conhost's specific FileVersion in every case conhost_mod_version = 0 # keep it 0 as a default - # special use cases - - # Win10_18363 is not recognized by windows.info as 18363 - # because all kernel file headers and debug structures report 18363 as - # "10.0.18362.1198" with the last part being incremented. However, we can use - # os_distinguisher to differentiate between 18362 and 18363 - if vers_minor_version == 18362 and is_18363_or_later: - vollog.debug( - "Detected 18363 data structures: working with 18363 symbol table." - ) - vers_minor_version = 18363 - # we need to define additional version numbers (which are then found via conhost.exe's FileVersion header) in case there is # ambiguity _within_ an OS version. If such a version number (last number of the tuple) is defined for the current OS # we need to inspect conhost.exe's headers to see if we can grab the precise version @@ -240,24 +238,40 @@ def determine_conhost_version( vollog.debug( "Requiring further version inspection due to OS version by checking conhost.exe's FileVersion header" ) - # the following is IntelLayer specific and might need to be adapted to other architectures. - physical_layer_name = context.layers[layer_name].config.get( - "memory_layer", None + + pe_table_name = intermed.IntermediateSymbolTable.create( + context, + configuration.path_join(config_path, "conhost"), + "windows", + "pe", + class_types=pe.class_types ) - if physical_layer_name: - ver = verinfo.VerInfo.find_version_info( - context, physical_layer_name, "CONHOST.EXE" + + try: + (major, minor, product, build) = verinfo.VerInfo.get_version_information( + context, pe_table_name, conhost_layer_name, conhost_base + ) + conhost_mod_version = build + vollog.debug(f"Found conhost.exe version {major}.{minor}.{product}.{build} in {conhost_layer_name} at base {conhost_base:#x}") + except (exceptions.InvalidAddressException, TypeError, AttributeError): + # the following is IntelLayer specific and might need to be adapted to other architectures. + physical_layer_name = context.layers[layer_name].config.get( + "memory_layer", None ) + if physical_layer_name: + ver = verinfo.VerInfo.find_version_info( + context, physical_layer_name, "CONHOST.EXE" + ) - if ver: - conhost_mod_version = ver[3] - vollog.debug( - "Determined conhost.exe's FileVersion: {}".format( - conhost_mod_version + if ver: + conhost_mod_version = ver[3] + vollog.debug( + "Determined conhost.exe's FileVersion: {}".format( + conhost_mod_version + ) ) - ) - else: - vollog.debug("Could not determine conhost.exe's FileVersion.") + else: + vollog.debug("Could not determine conhost.exe's FileVersion.") else: vollog.debug( "Unable to retrieve physical memory layer, skipping FileVersion check." @@ -287,6 +301,7 @@ def determine_conhost_version( for nt_maj, nt_min, vers_min, conhost_ver in version_dict if nt_maj == nt_major_version and nt_min == nt_minor_version + and vers_min <= vers_minor_version and conhost_ver <= conhost_mod_version ] current_versions.sort() @@ -321,6 +336,8 @@ def create_conhost_symbol_table( layer_name: str, nt_symbol_table: str, config_path: str, + conhost_layer_name: str, + conhost_base: int, ) -> str: """Creates a symbol table for conhost structures. @@ -339,13 +356,16 @@ def create_conhost_symbol_table( context, layer_name, nt_symbol_table, + config_path, + conhost_layer_name, + conhost_base, ) vollog.debug(f"Using symbol file '{symbol_filename}' and types {class_types}") return intermed.IntermediateSymbolTable.create( context, - config_path, + configuration.path_join(config_path, "conhost"), os.path.join("windows", "consoles"), symbol_filename, class_types=class_types, @@ -383,9 +403,7 @@ def get_console_info( that console information structure. """ - conhost_symbol_table = cls.create_conhost_symbol_table( - context, kernel_layer_name, kernel_table_name, config_path - ) + conhost_symbol_table = None for conhost_proc, proc_layer_name in cls.find_conhost_proc(procs): if not conhost_proc: @@ -407,6 +425,16 @@ def get_console_info( proc_layer = context.layers[proc_layer_name] + if conhost_symbol_table is None: + conhost_symbol_table = cls.create_conhost_symbol_table( + context, + kernel_layer_name, + kernel_table_name, + config_path, + proc_layer_name, + conhostexe_base + ) + conhost_module = context.module( conhost_symbol_table, proc_layer_name, offset=conhostexe_base ) @@ -560,6 +588,60 @@ def get_console_info( } ) + vollog.debug( + f"Getting ExeAliasList entries for {console_info.ExeAliasList}" + ) + console_properties.append( + { + "level": 1, + "name": "_CONSOLE_INFORMATION.ExeAliasList", + "address": console_info.ExeAliasList.vol.offset, + "data": "", + } + ) + if console_info.ExeAliasList: + for index, exe_alias_list in enumerate( + console_info.get_exe_aliases() + ): + try: + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ExeAliasList.AliasList_{index}", + "address": exe_alias_list.vol.offset, + "data": "", + } + ) + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ExeAliasList.AliasList_{index}.ExeName", + "address": exe_alias_list.ExeName.vol.offset, + "data": exe_alias_list.get_exename(), + } + ) + for alias_index, alias in enumerate(exe_alias_list.get_aliases()): + console_properties.append( + { + "level": 3, + "name": f"_CONSOLE_INFORMATION.ExeAliasList.AliasList_{index}.Alias_{alias_index}.Source", + "address": alias.Source.vol.offset, + "data": alias.get_source(), + } + ) + console_properties.append( + { + "level": 3, + "name": f"_CONSOLE_INFORMATION.ExeAliasList.AliasList_{index}.Alias_{alias_index}.Target", + "address": alias.Target.vol.offset, + "data": alias.get_target(), + } + ) + except Exception as e: + vollog.debug( + f"reading {exe_alias_list} encountered exception {e}" + ) + vollog.debug( f"Getting HistoryList entries for {console_info.HistoryList}" ) @@ -639,60 +721,66 @@ def get_console_info( f"reading {command_history} encountered exception {e}" ) - vollog.debug(f"Getting ScreenBuffer entries for {console_info}") - console_properties.append( - { - "level": 1, - "name": "_CONSOLE_INFORMATION.CurrentScreenBuffer", - "address": console_info.CurrentScreenBuffer.vol.offset, - "data": "", - } - ) - for screen_index, screen_info in enumerate( - console_info.get_screens() - ): - try: - console_properties.append( - { - "level": 2, - "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}", - "address": screen_info, - "data": "", - } - ) - console_properties.append( - { - "level": 2, - "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenX", - "address": None, - "data": screen_info.ScreenX, - } - ) - console_properties.append( - { - "level": 2, - "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenY", - "address": None, - "data": screen_info.ScreenY, - } - ) - console_properties.append( - { - "level": 2, - "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.Dump", - "address": None, - "data": "\n".join(screen_info.get_buffer()), - } - ) - except Exception as e: - vollog.debug( - f"reading {screen_info} encountered exception {e}" - ) + try: + vollog.debug(f"Getting ScreenBuffer entries for {console_info}") + console_properties.append( + { + "level": 1, + "name": "_CONSOLE_INFORMATION.CurrentScreenBuffer", + "address": console_info.CurrentScreenBuffer.vol.offset, + "data": "", + } + ) + for screen_index, screen_info in enumerate( + console_info.get_screens() + ): + try: + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}", + "address": screen_info, + "data": "", + } + ) + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenX", + "address": None, + "data": screen_info.ScreenX, + } + ) + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.ScreenY", + "address": None, + "data": screen_info.ScreenY, + } + ) + console_properties.append( + { + "level": 2, + "name": f"_CONSOLE_INFORMATION.ScreenBuffer_{screen_index}.Dump", + "address": None, + "data": "\n".join(screen_info.get_buffer()), + } + ) + except Exception as e: + vollog.debug( + f"reading {screen_info} encountered exception {e}" + ) + except Exception as e: + vollog.debug( + f"reading _CONSOLE_INFORMATION.CurrentScreenBuffer encountered exception {e}" + ) except exceptions.PagedInvalidAddressException as exp: vollog.debug( f"Required memory at {exp.invalid_address:#x} is not valid" ) + continue yield conhost_proc, console_info, console_properties @@ -776,6 +864,7 @@ def _generator( vollog.debug(f"Possible CommandHistorySize values: {max_history}") vollog.debug(f"Possible HistoryBufferMax values: {max_buffers}") + proc = None for proc, console_info, console_properties in self.get_console_info( self.context, kernel.layer_name, @@ -786,13 +875,14 @@ def _generator( max_buffers, ): process_name = utility.array_to_string(proc.ImageFileName) + process_pid = proc.UniqueProcessId if console_info and console_properties: for console_property in console_properties: yield ( console_property["level"], ( - proc.UniqueProcessId, + process_pid, process_name, format_hints.Hex(console_info.vol.offset), console_property["name"], @@ -804,6 +894,11 @@ def _generator( str(console_property["data"]), ), ) + else: + vollog.warn(f"_CONSOLE_INFORMATION not found for {process_name} with pid {process_pid}.") + + if proc is None: + vollog.warn("No conhost.exe processes found.") def _conhost_proc_filter(self, proc): """ @@ -811,7 +906,7 @@ def _conhost_proc_filter(self, proc): """ process_name = utility.array_to_string(proc.ImageFileName) - return process_name != "conhost.exe" + return process_name.lower() != "conhost.exe" def run(self): kernel = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 57b8dcd3fb..4a06ed0c95 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -13,7 +13,7 @@ from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, modules, dlllist +from volatility3.plugins.windows import pslist, modules vollog = logging.getLogger(__name__) 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 0a8cb57829..413b6f4663 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 @@ -152,8 +152,8 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" }, "offset": 1392 } @@ -562,7 +562,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -581,6 +582,70 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "ExeName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 24 + }, + "AliasList": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 48 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 2b98945c98..b3bf9666c6 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json @@ -152,8 +152,8 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" }, "offset": 1392 } @@ -562,7 +562,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -581,6 +582,70 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 16 + }, + "ExeName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 24 + }, + "AliasList": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 48 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 3d76d1cb4e..d962418d32 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json @@ -152,10 +152,10 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" }, - "offset": 1436 + "offset": -856 } }, "kind": "struct", @@ -552,7 +552,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -571,6 +572,70 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "ExeName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 24 + }, + "AliasList": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 48 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 681113b0f4..0963afee33 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json @@ -1,5 +1,5 @@ { - "symbols": {}, + "symbols": {}, "enums": {}, "base_types": { "unsigned long": { @@ -152,8 +152,8 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" }, "offset": 1436 } @@ -552,7 +552,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -571,6 +572,70 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "ExeName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 24 + }, + "AliasList": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 48 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 7bc6ae9d11..cee78e2b5e 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 @@ -152,8 +152,11 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } }, "offset": 2410 } @@ -562,7 +565,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -581,6 +585,104 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 f612c527aa..59e9fd3b97 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 @@ -152,8 +152,11 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } }, "offset": 9232 } @@ -562,7 +565,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -581,6 +585,104 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { 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 083a966a09..df02169cbf 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json @@ -152,8 +152,11 @@ }, "ExeAliasList": { "type": { - "kind": "base", - "name": "unsigned short" + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } }, "offset": -376 } @@ -562,7 +565,8 @@ } }, "offset": 88 - } }, + } + }, "kind": "struct", "size": 96 }, @@ -581,6 +585,104 @@ }, "kind": "struct", "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json new file mode 100644 index 0000000000..0dfaf34af4 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json @@ -0,0 +1,722 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 24 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 26 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 136 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 140 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1648 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 1616 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1664 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 1296 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 1272 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 1280 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -920 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -912 + }, + "ExeAliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -1008 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 4 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 32 + }, + "FirstRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 8 + }, + "LastRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 16 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 80 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": -88 + }, + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 0 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": -18 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": -20 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 16 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": -8 + } + }, + "kind": "struct", + "size": 480 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json new file mode 100644 index 0000000000..4c71ee32e4 --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json @@ -0,0 +1,722 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2400 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2402 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2512 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2516 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 2944 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 2912 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 3008 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 2632 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 2608 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2616 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 10640 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 10648 + }, + "ExeAliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 10552 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 4 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 32 + }, + "FirstRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 8 + }, + "LastRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 16 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": -80 + }, + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 0 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": -20 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 16 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": -16 + } + }, + "kind": "struct", + "size": 472 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json new file mode 100644 index 0000000000..d6da72ef2a --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json @@ -0,0 +1,722 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2400 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2402 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2512 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2516 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 2944 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 2912 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 3008 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 2632 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 2608 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2616 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 10664 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 10672 + }, + "ExeAliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 10576 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 4 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 8 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 6 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 32 + }, + "FirstRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 8 + }, + "LastRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 16 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 72 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": -96 + }, + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 0 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": -20 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 16 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": -8 + } + }, + "kind": "struct", + "size": 480 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json new file mode 100644 index 0000000000..120ad85ecb --- /dev/null +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json @@ -0,0 +1,723 @@ +{ + "symbols": {}, + "enums": {}, + "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" + }, + "short": { + "kind": "int", + "size": 2, + "signed": true, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_CONSOLE_INFORMATION": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2592 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 2594 + }, + "CommandHistorySize": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2704 + }, + "HistoryBufferMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2708 + }, + "OriginalTitle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 3056 + }, + "Title": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 3120 + }, + "GetScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 3216 + }, + "CurrentScreenBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 2824 + }, + "ConsoleProcessList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 2800 + }, + "ProcessCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 2808 + }, + "HistoryList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": -360 + }, + "HistoryBufferCount": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": -352 + }, + "ExeAliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 3776 + } + }, + "kind": "struct", + "size": 140 + }, + "_VECTOR": { + "fields": { + "Begin": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 0 + }, + "End": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_COMMAND" + } + }, + "offset": 8 + }, + "EndCapacity": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned long" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + }, + "_CONSOLE_PROCESS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ConsoleProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 24 + }, + "_CONSOLE_PROCESS_HANDLE": { + "fields": { + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 52 + }, + "_CONSOLE_PROCESS": { + "fields": { + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 28 + }, + "ThreadId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + }, + "ProcessHandle": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 24 + }, + "_COMMAND_HISTORY": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "CommandBucket": { + "type": { + "kind": "struct", + "name": "_VECTOR" + }, + "offset": 16 + }, + "CommandCountMax": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 40 + }, + "Application": { + "type": { + "kind": "struct", + "name": "_COMMAND" + }, + "offset": 48 + }, + "ConsoleProcessHandle": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CONSOLE_PROCESS_HANDLE" + } + }, + "offset": 80 + }, + "Flags": { + "type": { + "kind": "base", + "name": "unsigned short" + }, + "offset": 88 + }, + "LastDisplayed": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 92 + } + }, + "kind": "struct", + "size": 96 + }, + "_SCREEN_INFORMATION": { + "fields": { + "TextBufferInfo": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 56 + }, + "Next": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SCREEN_INFORMATION" + } + }, + "offset": 64 + } + }, + "kind": "struct", + "size": 72 + }, + "_ROW_POINTER": { + "fields": { + "Row": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + + }, + "_ROWS_ARRAY": { + "fields": { + "Rows": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_ROW_POINTER" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_TEXT_BUFFER_INFO": { + "fields": { + "ScreenX": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "ScreenY": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 10 + }, + "BufferRows": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROWS_ARRAY" + } + }, + "offset": 16 + }, + "BufferCapacity": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 10 + }, + "ThisBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": 40 + }, + "FirstRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 16 + }, + "LastRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": 24 + }, + "BufferStart": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 88 + } + }, + "kind": "struct", + "size": 72 + }, + "_CHAR_ROW_CELL": { + "fields": { + "Text": { + "type": { + "kind": "struct", + "name": "nt_symbols!_UNICODE_STRING" + }, + "offset": 0 + }, + "DbcsAttribute": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 2 + } + }, + "kind": "struct", + "size": 3 + }, + "_CHAR_ROW_CELL_ARRAY": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_ROW": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_ROW" + } + }, + "offset": -88 + }, + "CharRow": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_CHAR_ROW_CELL_ARRAY" + } + }, + "offset": 0 + }, + "RowLength": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Index": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": -20 + }, + "RowLength2": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 8 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "short" + }, + "offset": 16 + }, + "TextBuffer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_TEXT_BUFFER_INFO" + } + }, + "offset": -8 + } + }, + "kind": "struct", + "size": 464 + }, + "_DEQUE": { + "fields": { + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "void" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_EXE_ALIAS_LIST": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "ExeName": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "AliasList": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + } + }, + "offset": 56 + } + }, + "kind": "struct", + "size": 64 + }, + "_ALIAS": { + "fields": { + "ListEntry": { + "type": { + "kind": "struct", + "name": "nt_symbols!_LIST_ENTRY" + }, + "offset": 0 + }, + "Source": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 16 + }, + "Target": { + "type": { + "kind": "struct", + "name": "_ALIAS_STRING" + }, + "offset": 48 + } + }, + "kind": "struct", + "size": 32 + }, + "_ALIAS_STRING": { + "fields": { + "Chars": { + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned char" + } + }, + "offset": 0 + }, + "Pointer": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "string" + } + }, + "offset": 0 + }, + "Length": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 16 + }, + "Allocated": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 24 + } + }, + "kind": "struct", + "size": 32 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "Dave Lassalle", + "datetime": "2024-07-31T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 23670bb9b3..70ef15093f 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -9,30 +9,42 @@ class ROW(objects.StructType): """A Row Structure.""" - def _valid_dbcs(self, c): + def _valid_dbcs(self, c, n): # TODO this need more research and testing # https://github.com/search?q=repo%3Amicrosoft%2Fterminal+DbcsAttr&type=code - valid = c in ( + valid = n == 0 and c in ( 0x0, 0x1, 0x2, + 0x8, + 0x10, + 0x18, 0x20, 0x28, 0x30, 0x48, 0x50, + 0x58, 0x60, + 0x68, + 0x70, + 0x78, 0x80, + 0x88, 0xA8, + 0xB8, 0xC0, 0xC8, 0x98, + 0xD8, + 0xE0, + 0xE8, 0xF8, 0xF0, 0xA0, ) - # if not valid: - # print("Bad Dbcs Attribute {}".format(hex(c))) + if n == 0 and not valid: + print("Bad Dbcs Attribute {}".format(hex(c))) return valid def get_text(self, truncate=True): @@ -50,10 +62,9 @@ def get_text(self, truncate=True): try: if char_row: line = "".join( - # "{} {} =".format(char_row[i:i + 2].decode('utf-16le', errors='replace'), char_row[i+2]) if self._valid_dbcs(char_row[i+2]) else "" ( char_row[i : i + 2].decode("utf-16le", errors="replace") - if self._valid_dbcs(char_row[i + 2]) + if self._valid_dbcs(char_row[i + 2], char_row[i+1]) else "" ) for i in range(0, len(char_row), 3) @@ -68,12 +79,78 @@ def get_text(self, truncate=True): return line +class ALIAS(objects.StructType): + """An Alias Structure""" + + def get_source(self): + if self.Source.Length < 8: + return self.Source.Chars.cast( + "string", + encoding="utf-16", + errors="replace", + max_length=self.Source.Length * 2, + ) + elif self.Source.Length < 1024: + return self.Source.Pointer.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + + def get_target(self): + if self.Target.Length < 8: + return self.Target.Chars.cast( + "string", + encoding="utf-16", + errors="replace", + max_length=self.Target.Length * 2, + ) + elif self.Target.Length < 1024: + return self.Target.Pointer.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + +class EXE_ALIAS_LIST(objects.StructType): + """An Exe Alias List Structure""" + + def get_exename(self): + exe_name = self.ExeName + # Windows 10 22000 and Server 20348 removed the Pointer + if isinstance(exe_name, objects.Pointer): + exe_name = exe_name.dereference() + return exe_name.get_string() + + if self.ExeName.Length < 8: + return self.ExeName.Chars.cast( + "string", + encoding="utf-16", + errors="replace", + max_length=self.ExeName.Length * 2, + ) + elif self.ExeName.Length < 1024: + return self.ExeName.Pointer.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) + + def get_aliases(self): + """Generator for the individual aliases for a + particular executable.""" + for alias in self.AliasList.to_list( + f"{self.get_symbol_table_name()}{constants.BANG}_ALIAS", + "ListEntry", + ): + yield alias + + class SCREEN_INFORMATION(objects.StructType): """A Screen Information Structure.""" @property def ScreenX(self): - return self.TextBufferInfo.BufferRows.Rows[0].Row.RowLength2 + # 22000 change from an array of pointers to _ROW to an array of _ROW + row = self.TextBufferInfo.BufferRows.Rows[0] + if hasattr(row, "Row"): + return row.Row.RowLength2 + else: + return row.RowLength2 @property def ScreenY(self): @@ -127,7 +204,9 @@ def get_buffer(self, truncate_rows=True, truncate_lines=True): for i in range(capacity): index = (start + i) % capacity - row = buffer_rows.Rows[index].Row + row = buffer_rows.Rows[index] + if hasattr(row, "Row"): + row = row.Row try: text = row.get_text(truncate_lines) rows.append(text) @@ -138,9 +217,9 @@ def get_buffer(self, truncate_rows=True, truncate_lines=True): rows = self._truncate_rows(rows) if rows: - rows = ["=== Start of buffer ==="] + rows + ["=== End of buffer ==="] + rows = ["=== START OF BUFFER ==="] + rows + ["=== END OF BUFFER ==="] else: - rows = ["=== No buffer data found ==="] + rows = ["=== NO BUFFER DATA FOUND ==="] return rows @@ -198,6 +277,17 @@ def get_histories(self): ): yield cmd_hist + def get_exe_aliases(self): + exe_alias_list = self.ExeAliasList + # Windows 10 22000 and Server 20348 made this a Pointer + if isinstance(exe_alias_list, objects.Pointer): + exe_alias_list = exe_alias_list.dereference() + for exe_alias_list_item in exe_alias_list.to_list( + f"{self.get_symbol_table_name()}{constants.BANG}_EXE_ALIAS_LIST", + "ListEntry" + ): + yield exe_alias_list_item + def get_processes(self): for proc in self.ConsoleProcessList.dereference().to_list( f"{self.get_symbol_table_name()}{constants.BANG}_CONSOLE_PROCESS_LIST", @@ -331,6 +421,8 @@ def get_commands(self): win10_x64_class_types = { + "_EXE_ALIAS_LIST": EXE_ALIAS_LIST, + "_ALIAS": ALIAS, "_ROW": ROW, "_SCREEN_INFORMATION": SCREEN_INFORMATION, "_CONSOLE_INFORMATION": CONSOLE_INFORMATION, From 9d98ab9b6548cfc5b36c3e6ade3425ea87498442 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Fri, 27 Sep 2024 10:39:57 -0500 Subject: [PATCH 130/250] #816 - black fixes --- .../framework/plugins/windows/consoles.py | 26 +++++++++++++------ .../symbols/windows/extensions/consoles.py | 5 ++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 46699aa31f..89f9fcc802 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -244,15 +244,19 @@ def determine_conhost_version( configuration.path_join(config_path, "conhost"), "windows", "pe", - class_types=pe.class_types + class_types=pe.class_types, ) try: - (major, minor, product, build) = verinfo.VerInfo.get_version_information( - context, pe_table_name, conhost_layer_name, conhost_base + (major, minor, product, build) = ( + verinfo.VerInfo.get_version_information( + context, pe_table_name, conhost_layer_name, conhost_base + ) ) conhost_mod_version = build - vollog.debug(f"Found conhost.exe version {major}.{minor}.{product}.{build} in {conhost_layer_name} at base {conhost_base:#x}") + vollog.debug( + f"Found conhost.exe version {major}.{minor}.{product}.{build} in {conhost_layer_name} at base {conhost_base:#x}" + ) except (exceptions.InvalidAddressException, TypeError, AttributeError): # the following is IntelLayer specific and might need to be adapted to other architectures. physical_layer_name = context.layers[layer_name].config.get( @@ -432,7 +436,7 @@ def get_console_info( kernel_table_name, config_path, proc_layer_name, - conhostexe_base + conhostexe_base, ) conhost_module = context.module( @@ -620,7 +624,9 @@ def get_console_info( "data": exe_alias_list.get_exename(), } ) - for alias_index, alias in enumerate(exe_alias_list.get_aliases()): + for alias_index, alias in enumerate( + exe_alias_list.get_aliases() + ): console_properties.append( { "level": 3, @@ -722,7 +728,9 @@ def get_console_info( ) try: - vollog.debug(f"Getting ScreenBuffer entries for {console_info}") + vollog.debug( + f"Getting ScreenBuffer entries for {console_info}" + ) console_properties.append( { "level": 1, @@ -895,7 +903,9 @@ def _generator( ), ) else: - vollog.warn(f"_CONSOLE_INFORMATION not found for {process_name} with pid {process_pid}.") + vollog.warn( + f"_CONSOLE_INFORMATION not found for {process_name} with pid {process_pid}." + ) if proc is None: vollog.warn("No conhost.exe processes found.") diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 70ef15093f..555197fc43 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -64,7 +64,7 @@ def get_text(self, truncate=True): line = "".join( ( char_row[i : i + 2].decode("utf-16le", errors="replace") - if self._valid_dbcs(char_row[i + 2], char_row[i+1]) + if self._valid_dbcs(char_row[i + 2], char_row[i + 1]) else "" ) for i in range(0, len(char_row), 3) @@ -108,6 +108,7 @@ def get_target(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + class EXE_ALIAS_LIST(objects.StructType): """An Exe Alias List Structure""" @@ -284,7 +285,7 @@ def get_exe_aliases(self): exe_alias_list = exe_alias_list.dereference() for exe_alias_list_item in exe_alias_list.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_EXE_ALIAS_LIST", - "ListEntry" + "ListEntry", ): yield exe_alias_list_item From bd678eaf80482d4285e3409288a7dfd116dccd82 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Fri, 27 Sep 2024 16:07:44 -0500 Subject: [PATCH 131/250] #816 - fix cmdscan --- .../framework/plugins/windows/cmdscan.py | 78 ++++++++++++------- .../framework/plugins/windows/consoles.py | 38 +++++---- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index b92d7efabc..09cb0f3163 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -14,17 +14,9 @@ from volatility3.framework.layers import scanners from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, vadinfo, info, verinfo, consoles -from volatility3.plugins.windows.registry import hivelist +from volatility3.plugins.windows import pslist, consoles -try: - import capstone - - has_capstone = True -except ImportError: - has_capstone = False - vollog = logging.getLogger(__name__) @@ -32,6 +24,7 @@ class CmdScan(interfaces.plugins.PluginInterface): """Looks for Windows Command History lists""" _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) @classmethod def get_requirements(cls): @@ -46,7 +39,7 @@ def get_requirements(cls): name="pslist", component=pslist.PsList, version=(2, 0, 0) ), requirements.PluginRequirement( - name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + name="consoles", plugin=consoles.Consoles, version=(1, 0, 0) ), requirements.BooleanRequirement( name="no_registry", @@ -70,7 +63,7 @@ def get_filtered_vads( size_filter: Optional[int] = 0x40000000, ) -> List[Tuple[int, int]]: """ - Returns vads of a process with smaller than size_filter + Returns vads of a process with size smaller than size_filter Args: conhost_proc: the process object for conhost.exe @@ -114,12 +107,10 @@ def get_command_history( Returns: The conhost process object, the command history structure, a dictionary of properties for - that command historyn structure. + that command history structure. """ - conhost_symbol_table = consoles.Consoles.create_conhost_symbol_table( - context, kernel_layer_name, kernel_table_name, config_path - ) + conhost_symbol_table = None for conhost_proc, proc_layer_name in consoles.Consoles.find_conhost_proc(procs): if not conhost_proc: @@ -143,11 +134,22 @@ def get_command_history( proc_layer = context.layers[proc_layer_name] + if conhost_symbol_table is None: + conhost_symbol_table = consoles.Consoles.create_conhost_symbol_table( + context, + kernel_layer_name, + kernel_table_name, + config_path, + proc_layer_name, + conhostexe_base, + ) + conhost_module = context.module( conhost_symbol_table, proc_layer_name, offset=conhostexe_base ) sections = cls.get_filtered_vads(conhost_proc) + found_history_for_proc = False # scan for potential _COMMAND_HISTORY structures by using the CommandHistorySize for max_history_value in max_history: max_history_bytes = struct.pack("H", max_history_value) @@ -258,7 +260,12 @@ def get_command_history( f"reading {command_history} encountered exception {e}" ) - yield conhost_proc, command_history, command_history_properties + if command_history and command_history_properties: + found_history_for_proc = True + yield conhost_proc, command_history, command_history_properties + + if not found_history_for_proc: + yield conhost_proc, command_history or None, [] def _generator( self, procs: Generator[interfaces.objects.ObjectInterface, None, None] @@ -276,19 +283,18 @@ def _generator( no_registry = self.config.get("no_registry") if no_registry is False: - max_history, _max_buffers = ( - consoles.Consoles.get_console_settings_from_registry( - self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - max_history, - [], - ) + max_history, _ = consoles.Consoles.get_console_settings_from_registry( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + max_history, + [], ) vollog.debug(f"Possible CommandHistorySize values: {max_history}") + proc = None for ( proc, command_history, @@ -302,13 +308,14 @@ def _generator( max_history, ): process_name = utility.array_to_string(proc.ImageFileName) + process_pid = proc.UniqueProcessId if command_history and command_history_properties: for command_history_property in command_history_properties: yield ( command_history_property["level"], ( - proc.UniqueProcessId, + process_pid, process_name, format_hints.Hex(command_history.vol.offset), command_history_property["name"], @@ -322,6 +329,25 @@ def _generator( str(command_history_property["data"]), ), ) + else: + yield ( + 0, + ( + process_pid, + process_name, + ( + format_hints.Hex(command_history.vol.offset) + if command_history + else renderers.NotApplicableValue() + ), + "_COMMAND_HISTORY", + renderers.NotApplicableValue(), + "History Not Found", + ), + ) + + if proc is None: + vollog.warn("No conhost.exe processes found.") def _conhost_proc_filter(self, proc): """ diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index 89f9fcc802..a2eade321c 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -17,19 +17,11 @@ from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows import pdbutil from volatility3.framework.symbols.windows.extensions import pe, consoles -from volatility3.plugins.windows import pslist, vadinfo, info, verinfo +from volatility3.plugins.windows import pslist, info, verinfo from volatility3.plugins.windows.registry import hivelist -try: - import capstone - - has_capstone = True -except ImportError: - has_capstone = False - vollog = logging.getLogger(__name__) @@ -37,6 +29,7 @@ class Consoles(interfaces.plugins.PluginInterface): """Looks for Windows console buffers""" _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) @classmethod def get_requirements(cls): @@ -53,9 +46,6 @@ def get_requirements(cls): requirements.VersionRequirement( name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) ), - requirements.VersionRequirement( - name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) - ), requirements.PluginRequirement( name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) ), @@ -443,6 +433,7 @@ def get_console_info( conhost_symbol_table, proc_layer_name, offset=conhostexe_base ) + found_console_info_for_proc = False # scan for potential _CONSOLE_INFORMATION structures by using the CommandHistorySize for max_history_value in max_history: max_history_bytes = struct.pack("H", max_history_value) @@ -790,7 +781,12 @@ def get_console_info( ) continue - yield conhost_proc, console_info, console_properties + if console_info and console_properties: + found_console_info_for_proc = True + yield conhost_proc, console_info, console_properties + + if not found_console_info_for_proc: + yield conhost_proc, console_info or None, [] @classmethod def get_console_settings_from_registry( @@ -903,8 +899,20 @@ def _generator( ), ) else: - vollog.warn( - f"_CONSOLE_INFORMATION not found for {process_name} with pid {process_pid}." + yield ( + 0, + ( + process_pid, + process_name, + ( + format_hints.Hex(console_info.vol.offset) + if console_info + else renderers.NotApplicableValue() + ), + "_CONSOLE_INFORMATION", + renderers.NotApplicableValue(), + "Console Information Not Found", + ), ) if proc is None: From f1200c85b7e6cd3e1752523d478721c288e9d91b Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 27 Sep 2024 16:14:43 -0500 Subject: [PATCH 132/250] Windows: PsList type-hint cleanup It's more appropriate to use `Iterator` than `Iterable` as a return type here. Also updates the `Iterator` item type to be `EPROCESS` instead of just `ObjectInterface`. --- volatility3/framework/plugins/windows/pslist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 8234b210f2..3411c618cb 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -4,7 +4,7 @@ import datetime import logging -from typing import Callable, Iterable, List, Type +from typing import Callable, Iterator, List, Type, TYPE_CHECKING from volatility3.framework import renderers, interfaces, layers, exceptions, constants from volatility3.framework.configuration import requirements @@ -12,6 +12,7 @@ from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe +from volatility3.framework.symbols.windows import extensions from volatility3.plugins import timeliner vollog = logging.getLogger(__name__) @@ -197,7 +198,7 @@ def list_processes( filter_func: Callable[ [interfaces.objects.ObjectInterface], bool ] = lambda _: False, - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterator["extensions.EPROCESS"]: """Lists all the processes in the primary layer that are in the pid config option. From 4b0ece0fc992c27bea47b951b83219e19d633194 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 27 Sep 2024 16:17:02 -0500 Subject: [PATCH 133/250] Windows: psxview module type-hints and cleanup Adds type-hints to methods throughout the psxview module. --- .../framework/plugins/windows/psxview.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 71919c4101..18ab3ca2f4 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,9 +1,13 @@ -import datetime, logging, string +import datetime +import logging +import string +from typing import Dict, Iterable, List 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.framework.interfaces import plugins +from volatility3.framework.renderers import TreeGrid, format_hints +from volatility3.framework.symbols.windows import extensions from volatility3.plugins.windows import ( handles, info, @@ -77,14 +81,16 @@ def _is_valid_proc_name(self, str): return False return True - def _filter_garbage_procs(self, proc_list): + def _filter_garbage_procs( + self, proc_list: Iterable[extensions.EPROCESS] + ) -> List[extensions.EPROCESS]: return [ p for p in proc_list if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) ] - def _translate_offset(self, offset): + def _translate_offset(self, offset: int) -> int: if not self.config["physical-offsets"]: return offset @@ -100,21 +106,25 @@ def _translate_offset(self, offset): return offset - def _proc_list_to_dict(self, tasks): + def _proc_list_to_dict( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: tasks = self._filter_garbage_procs(tasks) return {self._translate_offset(proc.vol.offset): proc for proc in 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: str, symbol_table: str + ) -> Dict[int, extensions.EPROCESS]: res = psscan.PsScan.scan_processes( context=self.context, layer_name=layer_name, symbol_table=symbol_table ) return self._proc_list_to_dict(res) - def _check_thrdscan(self): + def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: ret = [] for ethread in thrdscan.ThrdScan.scan_threads( @@ -135,8 +145,10 @@ def _check_thrdscan(self): return self._proc_list_to_dict(ret) - def _check_csrss_handles(self, tasks, layer_name, symbol_table): - ret = [] + def _check_csrss_handles( + self, tasks: Iterable[extensions.EPROCESS], layer_name: str, symbol_table: str + ) -> Dict[int, extensions.EPROCESS]: + ret: List[extensions.EPROCESS] = [] for p in tasks: name = self._proc_name_to_string(p) From 3ebee83d05ca6d82361b85fa265a8de64bafff44 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 27 Sep 2024 16:18:27 -0500 Subject: [PATCH 134/250] Windows: psxview cleanup Replaced this series of statements with a single invocation of the `all` builtin for brevity and conciseness. --- volatility3/framework/plugins/windows/psxview.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 18ab3ca2f4..b7a372a402 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -75,11 +75,8 @@ def _proc_name_to_string(self, proc): "string", max_length=proc.ImageFileName.vol.count, errors="replace" ) - 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 _is_valid_proc_name(self, string: str) -> bool: + return all(c in self.valid_proc_name_chars for c in string) def _filter_garbage_procs( self, proc_list: Iterable[extensions.EPROCESS] From d38a69fd5c4813abe1239783550798ba163c38a2 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 29 Sep 2024 19:49:21 +0100 Subject: [PATCH 135/250] Ensure the class for Modules is stored in the configruation --- volatility3/framework/interfaces/context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index a95f2b4646..8b5e816e8a 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -191,6 +191,9 @@ def build_configuration(self) -> "interfaces.configuration.HierarchicalDict": self._native_layer_name ].build_configuration() + # Modules are constructable, and therefore require a class configuration variable + config["class"] = self.__class__.__module__ + "." + self.__class__.__name__ + for subconfig in subconfigs: for req in subconfigs[subconfig]: config[interfaces.configuration.path_join(subconfig, req)] = subconfigs[ From 49dc89c30cc005ec2a1fed7d697206c7aafc12d2 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 27 Sep 2024 16:19:27 -0500 Subject: [PATCH 136/250] Windows: psxview Win10+ fix The current implementation of this plugin does not incorporate the cookie on Win10+ systems, causing it to fail. This commit fixes that issue, and also contributes a performance improvement by extracting the call to `handles_plugin.get_type_map()` from the loop. Also replaces for-loop with list comprehension for clarity. --- .../framework/plugins/windows/pslist.py | 2 +- .../framework/plugins/windows/psxview.py | 73 +++++++++---------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 3411c618cb..478cc8b1b6 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -4,7 +4,7 @@ import datetime import logging -from typing import Callable, Iterator, List, Type, TYPE_CHECKING +from typing import Callable, Iterator, List, Type from volatility3.framework import renderers, interfaces, layers, exceptions, constants from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index b7a372a402..a8d185a2c0 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,6 +1,7 @@ import datetime import logging import string +from itertools import chain from typing import Dict, Iterable, List from volatility3.framework import constants, exceptions @@ -147,30 +148,33 @@ def _check_csrss_handles( ) -> Dict[int, extensions.EPROCESS]: ret: List[extensions.EPROCESS] = [] + handles_plugin = handles.Handles( + context=self.context, config_path=self.config_path + ) + + type_map = handles_plugin.get_type_map(self.context, layer_name, symbol_table) + + cookie = handles_plugin.find_cookie( + context=self.context, + layer_name=layer_name, + symbol_table=symbol_table, + ) + 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" - ) + if name != "csrss.exe": + continue + + try: + ret += [ + handle.Body.cast("_EPROCESS") + for handle in handles_plugin.handles(p.ObjectTable) + if handle.get_object_type(type_map, cookie) == "Process" + ] + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVV, "Cannot access eprocess object table" + ) return self._proc_list_to_dict(ret) @@ -187,7 +191,7 @@ def _generator(self): ) # get processes from each source - processes = {} + processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} processes["pslist"] = self._check_pslist(kdbg_list_processes) processes["psscan"] = self._check_psscan(layer_name, symbol_table) @@ -196,27 +200,20 @@ def _generator(self): kdbg_list_processes, layer_name, symbol_table ) - # print results - - # list of lists of offsets - offsets = [list(processes[source].keys()) for source in processes] - - # flatten to one list - offsets = sum(offsets, []) - - # remove duplicates - offsets = set(offsets) + # Unique set of all offsets from all sources + offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) for offset in offsets: - proc = None + # We know there will be at least one process mapped to each offset + proc: extensions.EPROCESS = next( + mapping[offset] for mapping in processes.values() if offset in mapping + ) in_sources = {src: False for src in processes} - for source in processes: - if offset in processes[source]: + for source, process_mapping in processes.items(): + if offset in process_mapping: in_sources[source] = True - if not proc: - proc = processes[source][offset] pid = proc.UniqueProcessId name = self._proc_name_to_string(proc) From d9dc28d0b5a05db8af852673ebefc5aab8257569 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 12:52:44 -0500 Subject: [PATCH 137/250] Windows: fix missing TCP connections This fixes some missing TCP connections in Windows 10 20348 by adding a scan constraint for `TTcb` pool tags. --- volatility3/framework/plugins/windows/netscan.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/netscan.py b/volatility3/framework/plugins/windows/netscan.py index 868bd8bcd1..66a24da5ac 100644 --- a/volatility3/framework/plugins/windows/netscan.py +++ b/volatility3/framework/plugins/windows/netscan.py @@ -76,7 +76,7 @@ def create_netscan_constraints( # ~ vollog.debug("Using pool size constraints: TcpL {}, TcpE {}, UdpA {}".format(tcpl_size, tcpe_size, udpa_size)) - return [ + constraints = [ # TCP listener poolscanner.PoolConstraint( b"TcpL", @@ -100,6 +100,19 @@ def create_netscan_constraints( ), ] + if symbol_table.startswith("netscan-win10-20348"): + vollog.debug("Adding additional pool constraint for `TTcb` tags") + constraints.append( + poolscanner.PoolConstraint( + b"TTcb", + type_name=symbol_table + constants.BANG + "_TCP_ENDPOINT", + size=(tcpe_size, None), + page_type=poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE, + ) + ) + + return constraints + @classmethod def determine_tcpip_version( cls, From 8ad592a490f23b27e6f79e1cbff7b67a7c2f8aba Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 11:00:58 -0500 Subject: [PATCH 138/250] Windows: Fixes bad callback validity check This fixes a bug in the callbacks plugin which causes it to miss `IoRegisterShutdownNotification` callbacks on x86b samples. The `header.NameInfo.Name` field was being incorrectly treated as the device type. This fixes the issue by updating the `is_valid` method on the `_SHUTDOWN_PACKET` extension type to take a `type_map` parameter, and updates the method to correctly validate the object type. --- .../framework/plugins/windows/callbacks.py | 8 ++++++-- .../symbols/windows/extensions/callbacks.py | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index d5eeda1ea3..562846def5 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -248,8 +248,12 @@ def scan( context, layer_name, nt_symbol_table, constraints ): try: - if hasattr(mem_object, "is_valid") and not mem_object.is_valid(): - continue + if isinstance(mem_object, callbacks._SHUTDOWN_PACKET): + if not mem_object.is_parseable(type_map): + continue + elif hasattr(mem_object, "is_valid"): + if not mem_object.is_valid(): + continue yield cls._process_scanned_callback(mem_object, type_map) except exceptions.InvalidAddressException: diff --git a/volatility3/framework/symbols/windows/extensions/callbacks.py b/volatility3/framework/symbols/windows/extensions/callbacks.py index f894644dbb..62e72803e2 100644 --- a/volatility3/framework/symbols/windows/extensions/callbacks.py +++ b/volatility3/framework/symbols/windows/extensions/callbacks.py @@ -1,4 +1,5 @@ import logging +from typing import Dict from volatility3.framework import exceptions, objects from volatility3.framework.symbols.windows.extensions import pool @@ -14,7 +15,7 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject): It exposes a function which sanity-checks structure members. """ - def is_valid(self) -> bool: + def is_parseable(self, type_map: Dict[int, str]) -> bool: """ Perform some checks. """ @@ -24,6 +25,9 @@ def is_valid(self) -> bool: and self.Entry.Blink.is_readable() and self.DeviceObject.is_readable() ): + vollog.debug( + f"Callback obj 0x{self.vol.offset:x} invalid due to unreadable structure members" + ) return False device = self.DeviceObject @@ -41,10 +45,17 @@ def is_valid(self) -> bool: try: header = device.get_object_header() - valid = header.NameInfo.Name == "Device" - return valid + object_type = header.get_object_type(type_map) + is_valid = object_type == "Device" + if not is_valid: + vollog.debug( + f"Callback obj 0x{self.vol.offset:x} invalid due to invalid device type: wanted 'Device', found '{object_type}'" + ) + return is_valid except ValueError: - vollog.debug(f"Could not get NameInfo for object at 0x{self.vol.offset:x}") + vollog.debug( + f"Could not get object type for object at 0x{self.vol.offset:x}" + ) return False From 806f78d3727b41152a2a70d4753f04012a5f8777 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 13:25:30 -0500 Subject: [PATCH 139/250] Linux: Sockstat - fix incorrect version bump I erred when I bumped the version as a part of !1271 - The `SockHandlers` class got the major version bump, not `Sockstat`. This reverts the `SockHandlers` version bump and applies it to `Sockstat` instead. --- volatility3/framework/plugins/linux/sockstat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index d3efc78dd8..652d039cf2 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -438,7 +438,7 @@ class Sockstat(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls): @@ -449,7 +449,7 @@ def get_requirements(cls): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="SockHandlers", component=SockHandlers, version=(1, 0, 0) + name="SockHandlers", component=SockHandlers, version=(2, 0, 0) ), requirements.PluginRequirement( name="lsof", plugin=lsof.Lsof, version=(1, 1, 0) From db55f23070b16d8c9df40e1ddfda5df8a6ad3282 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 16:55:27 -0500 Subject: [PATCH 140/250] Windows: Callbacks - fix breaking API change Moves as much of the `is_parseable` check as possible back into an `is_valid` method to avoid breaking API changes. --- .../symbols/windows/extensions/callbacks.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/callbacks.py b/volatility3/framework/symbols/windows/extensions/callbacks.py index 62e72803e2..f54db39f20 100644 --- a/volatility3/framework/symbols/windows/extensions/callbacks.py +++ b/volatility3/framework/symbols/windows/extensions/callbacks.py @@ -15,7 +15,7 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject): It exposes a function which sanity-checks structure members. """ - def is_parseable(self, type_map: Dict[int, str]) -> bool: + def is_valid(self) -> bool: """ Perform some checks. """ @@ -30,20 +30,32 @@ def is_parseable(self, type_map: Dict[int, str]) -> bool: ) return False - device = self.DeviceObject - if not device or not (device.DriverObject.DriverStart % 0x1000 == 0): - vollog.debug( - f"callback obj 0x{self.vol.offset:x} invalid due to invalid device object" - ) - return False - except exceptions.InvalidAddressException: vollog.debug( f"callback obj 0x{self.vol.offset:x} invalid due to invalid address access" ) return False + return True + + def is_parseable(self, type_map: Dict[int, str]) -> bool: + """ + Determines whether or not this `_SHUTDOWN_PACKET` callback can be reliably parsed. + Requires a `type_map` that maps NT executive object type indices to string representations. + This type map can be acquired via the `handles.Handles.get_type_map` classmethod. + """ + if not self.is_valid(): + return False + try: + + device = self.DeviceObject + if not device or not (device.DriverObject.DriverStart % 0x1000 == 0): + vollog.debug( + f"callback obj 0x{self.vol.offset:x} invalid due to invalid device object" + ) + return False + header = device.get_object_header() object_type = header.get_object_type(type_map) is_valid = object_type == "Device" @@ -52,6 +64,11 @@ def is_parseable(self, type_map: Dict[int, str]) -> bool: f"Callback obj 0x{self.vol.offset:x} invalid due to invalid device type: wanted 'Device', found '{object_type}'" ) return is_valid + except exceptions.InvalidAddressException: + vollog.debug( + f"callback obj 0x{self.vol.offset:x} invalid due to invalid address access" + ) + return False except ValueError: vollog.debug( f"Could not get object type for object at 0x{self.vol.offset:x}" From 6cd39c04c14735057c747c933eeccdf364bb7466 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 18 Sep 2024 17:04:17 +1000 Subject: [PATCH 141/250] Refactor of module object. Adding function helpers to simplify the control of exceptions and errors and consolidate everything on them. --- .../symbols/linux/extensions/__init__.py | 89 +++++++++---------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fdd34403ac..0a879249b4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,7 +13,6 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion -from volatility3.framework.configuration import requirements from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -56,88 +55,80 @@ def mod_mem_type(self): self._mod_mem_type = {} return self._mod_mem_type + def _get_mem_type(self, mod_mem_type_name): + module_mem_index = self.mod_mem_type.get(mod_mem_type_name) + if module_mem_index is None: + raise AttributeError(f"Unknown module memory type '{mod_mem_type_name}'") + + if not (0 <= module_mem_index < self.mem.count): + raise AttributeError( + f"Invalid module memory type index '{module_mem_index}'" + ) + + return self.mem[module_mem_index] + + def _get_mem_size(self, mod_mem_type_name): + return self._get_mem_type(mod_mem_type_name).size + + def _get_mem_base(self, mod_mem_type_name): + return self._get_mem_type(mod_mem_type_name).base + def get_module_base(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_base: Unable to get module base. Cannot read base from MOD_TEXT." - ) + return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): return self.core_layout.base elif self.has_member("module_core"): return self.module_core - raise AttributeError("module -> get_module_base: Unable to get module base") + + raise AttributeError("Unable to get module base") def get_init_size(self): if self.has_member("mem"): # kernels 6.4+ - try: - return ( - self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].size - + self.mem[self.mod_mem_type["MOD_INIT_DATA"]].size - + self.mem[self.mod_mem_type["MOD_INIT_RODATA"]].size - ) - except KeyError: - raise AttributeError( - "module -> get_init_size: Unable to determine .init section size of module. Cannot read size of MOD_INIT_TEXT, MOD_INIT_DATA, and MOD_INIT_RODATA" - ) + return ( + self._get_mem_size("MOD_INIT_TEXT") + + self._get_mem_size("MOD_INIT_DATA") + + self._get_mem_size("MOD_INIT_RODATA") + ) elif self.has_member("init_layout"): return self.init_layout.size elif self.has_member("init_size"): return self.init_size - raise AttributeError( - "module -> get_init_size: Unable to determine .init section size of module" - ) + + raise AttributeError("Unable to determine .init section size of module") def get_core_size(self): if self.has_member("mem"): # kernels 6.4+ - try: - return ( - self.mem[self.mod_mem_type["MOD_TEXT"]].size - + self.mem[self.mod_mem_type["MOD_DATA"]].size - + self.mem[self.mod_mem_type["MOD_RODATA"]].size - + self.mem[self.mod_mem_type["MOD_RO_AFTER_INIT"]].size - ) - except KeyError: - raise AttributeError( - "module -> get_core_size: Unable to determine core size of module. Cannot read size of MOD_TEXT, MOD_DATA, MOD_RODATA, and MOD_RO_AFTER_INIT." - ) + return ( + self._get_mem_size("MOD_TEXT") + + self._get_mem_size("MOD_DATA") + + self._get_mem_size("MOD_RODATA") + + self._get_mem_size("MOD_RO_AFTER_INIT") + ) elif self.has_member("core_layout"): return self.core_layout.size elif self.has_member("core_size"): return self.core_size - raise AttributeError( - "module -> get_core_size: Unable to determine core size of module" - ) + + raise AttributeError("Unable to determine core size of module") def get_module_core(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_core: Unable to get module core. Cannot read base from MOD_TEXT." - ) + return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): return self.core_layout.base elif self.has_member("module_core"): return self.module_core - raise AttributeError("module -> get_module_core: Unable to get module core") + raise AttributeError("Unable to get module core") def get_module_init(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_core: Unable to get module init. Cannot read base from MOD_INIT_TEXT." - ) + return self._get_mem_base("MOD_INIT_TEXT") elif self.has_member("init_layout"): return self.init_layout.base elif self.has_member("module_init"): return self.module_init - raise AttributeError("module -> get_module_init: Unable to get module init") + raise AttributeError("Unable to get module init") def get_name(self): """Get the name of the module as a string""" From ba351f511d9e4427c3376bfa5d312e8253f85d9a Mon Sep 17 00:00:00 2001 From: eve Date: Tue, 1 Oct 2024 06:36:56 +0100 Subject: [PATCH 142/250] Linux: Update malfind plugin to use symbols.symbol_table_is_64bit when determining if a 32bit OS is detected in the sample --- volatility3/framework/plugins/linux/malfind.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index cf06ee0ccb..18f3dcd56b 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -5,7 +5,7 @@ from typing import List import logging from volatility3.framework import constants, interfaces -from volatility3.framework import renderers +from volatility3.framework import renderers, symbols from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints @@ -63,15 +63,9 @@ def _list_injections(self, task): def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel vmlinux = self.context.modules[self.config["kernel"]] - if ( - self.context.symbol_space.get_type( - vmlinux.symbol_table_name + constants.BANG + "pointer" - ).size - == 4 - ): - is_32bit_arch = True - else: - is_32bit_arch = False + is_32bit_arch = not symbols.symbol_table_is_64bit( + self.context, vmlinux.symbol_table_name + ) for task in tasks: process_name = utility.array_to_string(task.comm) From 5dee3aeeee5e6b08481c1e39d2f7cb37a17c086c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 1 Oct 2024 19:30:50 +1000 Subject: [PATCH 143/250] Add linux.hidden_modules plugin --- .../framework/plugins/linux/hidden_modules.py | 331 ++++++++++++++++++ .../symbols/linux/extensions/__init__.py | 38 ++ 2 files changed, 369 insertions(+) create mode 100644 volatility3/framework/plugins/linux/hidden_modules.py diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py new file mode 100644 index 0000000000..1c2429449e --- /dev/null +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -0,0 +1,331 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import re +import functools +import logging +import contextlib +from typing import List, Iterable +from volatility3.framework import renderers, interfaces, exceptions, objects +from volatility3.framework.constants.architectures import LINUX_ARCHS +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import lsmod + +vollog = logging.getLogger(__name__) + + +class Hidden_modules(interfaces.plugins.PluginInterface): + """Carves memory to find hidden kernel modules""" + + _required_framework_version = (2, 10, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=LINUX_ARCHS, + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="fast", + description="Fast scan method. Recommended only for kernels 4.2 and above", + optional=True, + default=False, + ), + ] + + def _get_modules_memory_boundaries(self, vmlinux): + if vmlinux.has_symbol("mod_tree"): + mod_tree = vmlinux.object_from_symbol("mod_tree") + modules_addr_min = mod_tree.addr_min + modules_addr_max = mod_tree.addr_max + elif vmlinux.has_symbol("module_addr_min"): + modules_addr_min = vmlinux.object_from_symbol("module_addr_min") + modules_addr_max = vmlinux.object_from_symbol("module_addr_max") + + if isinstance(modules_addr_min, objects.Void): + # Crap ISF! Here's my best-effort workaround + vollog.warning( + "Your ISF symbols are missing type information. You may need to update " + "the ISF using the latest version of dwarf2json" + ) + # See issue #1041. In the Linux kernel these are "unsigned long" + for type_name in ("long unsigned int", "unsigned long"): + if vmlinux.has_type(type_name): + modules_addr_min = modules_addr_min.cast(type_name) + modules_addr_max = modules_addr_max.cast(type_name) + break + else: + raise exceptions.VolatilityException( + "Bad ISF! Please update the ISF using the latest version of dwarf2json" + ) + else: + raise exceptions.VolatilityException( + "Cannot find the module memory allocation area. Unsupported kernel" + ) + + return modules_addr_min, modules_addr_max + + def _get_module_state_live_bytes( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> bytes: + """Retrieve the MODULE_STATE_LIVE value bytes by introspecting its enum type + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + The MODULE_STATE_LIVE value bytes + """ + vmlinux = context.modules[vmlinux_module_name] + module_state_type_template = vmlinux.get_type("module").vol.members["state"][1] + module_state_live_val = module_state_type_template.choices["MODULE_STATE_LIVE"] + data_format = module_state_type_template.base_type.vol.data_format + module_state_live_bytes = objects.convert_value_to_data( + module_state_live_val, int, data_format + ) + return module_state_live_bytes + + def get_hidden_modules_vol2( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses, + modules_memory_boundaries: tuple, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Enumerate hidden modules using the traditional implementation. + + This is a port of the Volatility2 plugin, with minor code improvements. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Yields: + module objects + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + check_nums = ( + 3000, + 2800, + 2700, + 2500, + 2300, + 2100, + 2000, + 1500, + 1300, + 1200, + 1024, + 512, + 256, + 128, + 96, + 64, + 48, + 32, + 24, + ) + modules_addr_min, modules_addr_max = modules_memory_boundaries + modules_addr_min = modules_addr_min & ~0xFFF + modules_addr_max = (modules_addr_max & ~0xFFF) + vmlinux_layer.page_size + + check_bufs = [] + replace_bufs = [] + minus_size = vmlinux.get_type("pointer").size + null_pointer_bytes = b"\x00" * minus_size + for num in check_nums: + check_bufs.append(b"\x00" * num) + replace_bufs.append((b"\xff" * (num - minus_size)) + null_pointer_bytes) + + all_ffs = b"\xff" * 4096 + scan_list = [] + for page_addr in range( + modules_addr_min, modules_addr_max, vmlinux_layer.page_size + ): + content_fixed = all_ffs + with contextlib.suppress( + exceptions.InvalidAddressException, + exceptions.PagedInvalidAddressException, + ): + content = vmlinux_layer.read(page_addr, vmlinux_layer.page_size) + + all_nulls = all(x == 0 for x in content) + if content and not all_nulls: + content_fixed = content + for check_bytes, replace_bytes in zip(check_bufs, replace_bufs): + content_fixed = content_fixed.replace( + check_bytes, replace_bytes + ) + + scan_list.append(content_fixed) + + scan_buf = b"".join(scan_list) + del scan_list + + module_state_live_bytes = self._get_module_state_live_bytes( + context, vmlinux_module_name + ) + # f'strings cannot be combined with bytes literals + for cur_addr in re.finditer(b"(?=(%s))" % (module_state_live_bytes), scan_buf): + module_addr = modules_addr_min + cur_addr.start() + + if module_addr in known_module_addresses: + continue + + module = vmlinux.object("module", offset=module_addr, absolute=True) + if module and module.is_valid(): + yield module + + @functools.cached_property + def module_address_alignment(self) -> int: + """Obtain the module memory address alignment. This is only used with the fast scan method. + + struct module is aligned to the L1 cache line, which is typically 64 bytes for most + common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this + will still work. + + Returns: + The struct module alignment + """ + # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata + # The cached_property won't provide any benefits until then + return 64 + + def get_hidden_modules_fast( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses, + modules_memory_boundaries: tuple, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Enumerate hidden modules by taking advantage of memory address alignment patterns + + This technique is much faster and uses less memory than the traditional scan method + in Volatility2, but it doesn't work with older kernels. + + From kernels 4.2 struct module allocation are aligned to the L1 cache line size. + In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in + the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can + also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json + doesn't support this feature yet. + In kernels < 4.2, alignment attributes are absent in the struct module, meaning + alignment cannot be guaranteed. Therefore, for older kernels, it's better to use + the traditional scan technique. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Yields: + module objects + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + module_addr_min, module_addr_max = modules_memory_boundaries + + module_state_live_bytes = self._get_module_state_live_bytes( + context, vmlinux_module_name + ) + + for module_addr in range( + module_addr_min, module_addr_max, self.module_address_alignment + ): + if module_addr in known_module_addresses: + continue + + try: + # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() + module_state_bytes = vmlinux_layer.read( + module_addr, len(module_state_live_bytes) + ) + if module_state_bytes != module_state_live_bytes: + continue + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): + continue + + module = vmlinux.object("module", offset=module_addr, absolute=True) + if module and module.is_valid(): + yield module + + def _validate_alignment_patterns(self, addresses: Iterable[int]) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + + Returns: + True if all the addresses meet the alignment + """ + return all(addr % self.module_address_alignment == 0 for addr in addresses) + + def get_hidden_modules( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Enumerate hidden modules + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Yields: + module objects + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + known_module_addresses = { + vmlinux_layer.canonicalize(module.vol.offset) + for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) + } + + modules_memory_boundaries = self._get_modules_memory_boundaries(vmlinux) + + if self.config.get("fast"): + if self._validate_alignment_patterns(known_module_addresses): + scan_method = self.get_hidden_modules_fast + else: + vollog.warning( + f"Module addresses aren't aligned to {self.module_address_alignment} bytes. " + "Switching to the traditional scan method." + ) + scan_method = self.get_hidden_modules_vol2 + else: + scan_method = self.get_hidden_modules_vol2 + + yield from scan_method( + context, + vmlinux_module_name, + known_module_addresses, + modules_memory_boundaries, + ) + + def _generator(self): + vmlinux_module_name = self.config["kernel"] + for module in self.get_hidden_modules(self.context, vmlinux_module_name): + module_addr = module.vol.offset + module_name = module.get_name() or renderers.NotAvailableValue() + fields = (format_hints.Hex(module_addr), module_name) + yield (0, fields) + + def run(self): + headers = [ + ("Address", format_hints.Hex), + ("Name", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0a879249b4..b30634ca3e 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -35,6 +35,34 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization + def is_valid(self): + layer = self._context.layers[self.vol.layer_name] + # Make sure the entire module content is readable + if not layer.is_valid(self.vol.offset, self.vol.size): + return False + + if not self.state.is_valid_choice: + return False + + core_size = self.get_core_size() + if not ( + 1 <= core_size <= 20000000 + and core_size + self.get_init_size() >= 4096 + and 1 <= self.get_core_text_size() <= 20000000 + ): + return False + + if self.has_member("mkobj") and self.mkobj.has_member("mod"): + if not ( + self.mkobj + and self.mkobj.mod + and self.mkobj.mod.is_readable() + and self.mkobj.mod == self.vol.offset + ): + return False + + return True + @property def mod_mem_type(self): """Return the mod_mem_type enum choices if available or an empty dict if not""" @@ -112,6 +140,16 @@ def get_core_size(self): raise AttributeError("Unable to determine core size of module") + def get_core_text_size(self): + if self.has_member("mem"): # kernels 6.4+ + return self._get_mem_size("MOD_TEXT") + elif self.has_member("core_layout"): + return self.core_layout.text_size + elif self.has_member("core_text_size"): + return self.core_text_size + + raise AttributeError("Unable to determine core text size of module") + def get_module_core(self): if self.has_member("mem"): # kernels 6.4+ return self._get_mem_base("MOD_TEXT") From c0672f6c4b2769138da961f2d7c17f5c688b81ac Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 16:22:26 -0500 Subject: [PATCH 144/250] Linux: Sockstat - adds pytest This adds a pytest for the `linux.sockstat.Sockstat` plugin. --- test/test_volatility.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_volatility.py b/test/test_volatility.py index 6e54aa0535..847be88d9b 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -341,6 +341,17 @@ def test_linux_tty_check(image, volatility, python): assert out.count(b"\n") >= 5 assert rc == 0 +def test_linux_sockstat(image, volatility, python): + rc, out, err = runvol_plugin("linux.sockstat.Sockstat", image, volatility, python) + + assert out.count(b"AF_UNIX") >= 354 + assert out.count(b"AF_BLUETOOTH") >= 5 + assert out.count(b"AF_INET") >= 32 + assert out.count(b"AF_INET6") >= 20 + assert out.count(b"AF_PACKET") >= 1 + assert out.count(b"AF_NETLINK") >= 43 + assert rc == 0 + def test_linux_library_list(image, volatility, python): rc, out, err = runvol_plugin( From ece2dbe9a77947d31eaaf7da7cbb1a6cea5f330d Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Tue, 1 Oct 2024 13:00:15 -0500 Subject: [PATCH 145/250] #816 formatting fixes --- .../framework/plugins/windows/consoles.py | 4 ++-- .../symbols/windows/extensions/consoles.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index a2eade321c..dfc14f2d6d 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -168,7 +168,7 @@ def determine_conhost_version( raise NotImplementedError( "Kernel Debug Structure version format not supported!" ) - except: + except Exception: # unsure what to raise here. Also, it might be useful to add some kind of fallback, # either to a user-provided version or to another method to determine conhost.exe's version raise exceptions.VolatilityException( @@ -834,7 +834,7 @@ def get_console_settings_from_registry( max_history.add(value.decode_data()) elif val_name == "NumberOfHistoryBuffers": max_buffers.add(value.decode_data()) - except: + except Exception: continue return max_history, max_buffers diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 555197fc43..390b6858dd 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -95,6 +95,8 @@ def get_source(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + return None + def get_target(self): if self.Target.Length < 8: return self.Target.Chars.cast( @@ -108,6 +110,8 @@ def get_target(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + return None + class EXE_ALIAS_LIST(objects.StructType): """An Exe Alias List Structure""" @@ -131,6 +135,8 @@ def get_exename(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + return None + def get_aliases(self): """Generator for the individual aliases for a particular executable.""" @@ -211,7 +217,7 @@ def get_buffer(self, truncate_rows=True, truncate_lines=True): try: text = row.get_text(truncate_lines) rows.append(text) - except: + except Exception: break if truncate_rows: @@ -301,7 +307,7 @@ def get_title(self): return self.Title.dereference().cast( "string", encoding="utf-16", errors="replace", max_length=512 ) - except: + except Exception: return "" def get_original_title(self): @@ -309,7 +315,7 @@ def get_original_title(self): return self.OriginalTitle.dereference().cast( "string", encoding="utf-16", errors="replace", max_length=512 ) - except: + except Exception: return "" @@ -340,6 +346,8 @@ def get_command(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + return None + class COMMAND_HISTORY(objects.StructType): """A Command History Structure.""" @@ -387,6 +395,8 @@ def get_application(self): "string", encoding="utf-16", errors="replace", max_length=512 ) + return None + def scan_command_bucket(self, end=None): """Brute force print all strings pointed to by the CommandBucket entries by going to greater of EndCapacity or CommandCountMax*sizeof(_COMMAND)""" From 950ab3e201e0f31efeb496276e1d30169b091a2b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 1 Oct 2024 19:47:09 +0100 Subject: [PATCH 146/250] Add in additional microarchitectures for vmscan --- .../generic/vmcs/nehalem-architecture.json | 131 ++++++++++++++++++ .../vmcs/sandybridge-architecture.json | 131 ++++++++++++++++++ .../generic/vmcs/westmere-architecture.json | 131 ++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 volatility3/symbols/generic/vmcs/nehalem-architecture.json create mode 100644 volatility3/symbols/generic/vmcs/sandybridge-architecture.json create mode 100644 volatility3/symbols/generic/vmcs/westmere-architecture.json diff --git a/volatility3/symbols/generic/vmcs/nehalem-architecture.json b/volatility3/symbols/generic/vmcs/nehalem-architecture.json new file mode 100644 index 0000000000..ae0ab28633 --- /dev/null +++ b/volatility3/symbols/generic/vmcs/nehalem-architecture.json @@ -0,0 +1,131 @@ +{ + "base_types": { + "pointer": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned char": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 1 + }, + "unsigned long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 4 + }, + "unsigned long long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned short": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 2 + } + }, + "enums": {}, + "metadata": { + "format": "6.1.0", + "producer": { + "datetime": "2021-07-31T17:37:28.302702", + "name": "vmextract-by-hand", + "version": "0.0.1" + } + }, + "symbols": { + "revision_id": { + "address": 0, + "constant_data": "MTQ=" + } + }, + "user_types": { + "_VMCS": { + "fields": { + "ept": { + "offset": 232, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "executive_vmcs_ptr": { + "offset": 208, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr3": { + "offset": 736, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr4": { + "offset": 744, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_pdpte": { + "offset": 928, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "unsigned long long" + } + } + }, + "guest_physical_addr": { + "offset": 240, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr3": { + "offset": 832, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr4": { + "offset": 840, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vmcs_link_ptr": { + "offset": 248, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vpid": { + "offset": 752, + "type": { + "kind": "struct", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 4096 + } + } +} \ No newline at end of file diff --git a/volatility3/symbols/generic/vmcs/sandybridge-architecture.json b/volatility3/symbols/generic/vmcs/sandybridge-architecture.json new file mode 100644 index 0000000000..b2b3cfebfe --- /dev/null +++ b/volatility3/symbols/generic/vmcs/sandybridge-architecture.json @@ -0,0 +1,131 @@ +{ + "base_types": { + "pointer": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned char": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 1 + }, + "unsigned long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 4 + }, + "unsigned long long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned short": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 2 + } + }, + "enums": {}, + "metadata": { + "format": "6.1.0", + "producer": { + "datetime": "2021-07-31T17:37:28.311608", + "name": "vmextract-by-hand", + "version": "0.0.1" + } + }, + "symbols": { + "revision_id": { + "address": 0, + "constant_data": "MTY=" + } + }, + "user_types": { + "_VMCS": { + "fields": { + "ept": { + "offset": 232, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "executive_vmcs_ptr": { + "offset": 208, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr3": { + "offset": 736, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr4": { + "offset": 744, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_pdpte": { + "offset": 928, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "unsigned long long" + } + } + }, + "guest_physical_addr": { + "offset": 240, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr3": { + "offset": 832, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr4": { + "offset": 840, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vmcs_link_ptr": { + "offset": 248, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vpid": { + "offset": 752, + "type": { + "kind": "struct", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 4096 + } + } +} \ No newline at end of file diff --git a/volatility3/symbols/generic/vmcs/westmere-architecture.json b/volatility3/symbols/generic/vmcs/westmere-architecture.json new file mode 100644 index 0000000000..2769f70812 --- /dev/null +++ b/volatility3/symbols/generic/vmcs/westmere-architecture.json @@ -0,0 +1,131 @@ +{ + "base_types": { + "pointer": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned char": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 1 + }, + "unsigned long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 4 + }, + "unsigned long long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + }, + "unsigned short": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 2 + } + }, + "enums": {}, + "metadata": { + "format": "6.1.0", + "producer": { + "datetime": "2021-07-31T17:37:28.314801", + "name": "vmextract-by-hand", + "version": "0.0.1" + } + }, + "symbols": { + "revision_id": { + "address": 0, + "constant_data": "MTU=" + } + }, + "user_types": { + "_VMCS": { + "fields": { + "ept": { + "offset": 320, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "executive_vmcs_ptr": { + "offset": 208, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr3": { + "offset": 736, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_cr4": { + "offset": 744, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "guest_pdpte": { + "offset": 928, + "type": { + "count": 4, + "kind": "array", + "subtype": { + "kind": "struct", + "name": "unsigned long long" + } + } + }, + "guest_physical_addr": { + "offset": 328, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr3": { + "offset": 832, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "host_cr4": { + "offset": 840, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vmcs_link_ptr": { + "offset": 248, + "type": { + "kind": "struct", + "name": "unsigned long long" + } + }, + "vpid": { + "offset": 220, + "type": { + "kind": "struct", + "name": "unsigned short" + } + } + }, + "kind": "struct", + "size": 4096 + } + } +} \ No newline at end of file From d5e6e7c203f4e17390f0891fa4b70138d1fb97a5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 2 Oct 2024 09:43:27 +1000 Subject: [PATCH 147/250] Allow any module state value in both traditional and fast scan methods --- .../framework/plugins/linux/hidden_modules.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 1c2429449e..6d04c08afc 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -73,28 +73,29 @@ def _get_modules_memory_boundaries(self, vmlinux): return modules_addr_min, modules_addr_max - def _get_module_state_live_bytes( + def _get_module_state_values_bytes( self, context: interfaces.context.ContextInterface, vmlinux_module_name: str, - ) -> bytes: - """Retrieve the MODULE_STATE_LIVE value bytes by introspecting its enum type + ) -> List[bytes]: + """Retrieve the module state values bytes by introspecting its enum type Args: context: The context to retrieve required elements (layers, symbol tables) from vmlinux_module_name: The name of the kernel module on which to operate Returns: - The MODULE_STATE_LIVE value bytes + A list with the module state values bytes """ vmlinux = context.modules[vmlinux_module_name] module_state_type_template = vmlinux.get_type("module").vol.members["state"][1] - module_state_live_val = module_state_type_template.choices["MODULE_STATE_LIVE"] data_format = module_state_type_template.base_type.vol.data_format - module_state_live_bytes = objects.convert_value_to_data( - module_state_live_val, int, data_format - ) - return module_state_live_bytes + values = module_state_type_template.choices.values() + values_bytes = [ + objects.convert_value_to_data(value, int, data_format) + for value in sorted(values) + ] + return values_bytes def get_hidden_modules_vol2( self, @@ -174,11 +175,12 @@ def get_hidden_modules_vol2( scan_buf = b"".join(scan_list) del scan_list - module_state_live_bytes = self._get_module_state_live_bytes( + module_state_values_bytes = self._get_module_state_values_bytes( context, vmlinux_module_name ) + values_bytes_pattern = b"|".join(module_state_values_bytes) # f'strings cannot be combined with bytes literals - for cur_addr in re.finditer(b"(?=(%s))" % (module_state_live_bytes), scan_buf): + for cur_addr in re.finditer(b"(?=(%s))" % values_bytes_pattern, scan_buf): module_addr = modules_addr_min + cur_addr.start() if module_addr in known_module_addresses: @@ -235,7 +237,7 @@ def get_hidden_modules_fast( module_addr_min, module_addr_max = modules_memory_boundaries - module_state_live_bytes = self._get_module_state_live_bytes( + module_state_values_bytes = self._get_module_state_values_bytes( context, vmlinux_module_name ) @@ -248,9 +250,9 @@ def get_hidden_modules_fast( try: # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() module_state_bytes = vmlinux_layer.read( - module_addr, len(module_state_live_bytes) + module_addr, len(module_state_values_bytes[0]) ) - if module_state_bytes != module_state_live_bytes: + if module_state_bytes not in module_state_values_bytes: continue except ( exceptions.PagedInvalidAddressException, From c18818aa8d953f188e09dce7d5457488dbb6c4dc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 2 Oct 2024 10:38:01 +1000 Subject: [PATCH 148/250] Fix misuse of all() and any() functions across the codebase --- volatility3/cli/text_filter.py | 4 +-- volatility3/cli/volshell/generic.py | 2 +- volatility3/framework/automagic/pdbscan.py | 6 ++-- .../framework/configuration/requirements.py | 28 ++++++++----------- volatility3/framework/interfaces/objects.py | 2 +- volatility3/framework/layers/intel.py | 6 ++-- volatility3/framework/layers/registry.py | 6 ++-- volatility3/framework/layers/segmented.py | 6 ++-- volatility3/framework/objects/__init__.py | 6 ++-- .../framework/plugins/linux/check_afinfo.py | 4 +-- volatility3/framework/plugins/timeliner.py | 8 ++---- volatility3/framework/symbols/metadata.py | 4 +-- 12 files changed, 31 insertions(+), 51 deletions(-) diff --git a/volatility3/cli/text_filter.py b/volatility3/cli/text_filter.py index 948e969df5..3d69934e99 100644 --- a/volatility3/cli/text_filter.py +++ b/volatility3/cli/text_filter.py @@ -53,7 +53,7 @@ def filter( """Filters the row based on each of the column_filters""" if not self._filters: return False - found = any([column_filter.found(row) for column_filter in self._filters]) + found = any(column_filter.found(row) for column_filter in self._filters) return not found @@ -86,7 +86,7 @@ def found(self, row: List[Any]) -> bool: otherwise it is filtered. """ if self.column_num is None: - found = any([self.find(x) for x in row]) + found = any(self.find(x) for x in row) else: found = self.find(row[self.column_num]) if self.exclude: diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index b95129d19e..c5eac36e22 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -529,7 +529,7 @@ def create_configurable( val, interfaces.configuration.BasicTypes ) and not isinstance(val, list): if not isinstance(val, list) or all( - [isinstance(x, interfaces.configuration.BasicTypes) for x in val] + isinstance(x, interfaces.configuration.BasicTypes) for x in val ): raise TypeError( "Configurable values must be simple types (int, bool, str, bytes)" diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 06b2111b4e..7f38a23e1a 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -209,10 +209,8 @@ def test_physical_kernel( try: kvp = vlayer.mapping(kvo, 0) if any( - [ - (p == kernel["mz_offset"] and layer_name == physical_layer_name) - for (_, _, p, _, layer_name) in kvp - ] + (p == kernel["mz_offset"] and layer_name == physical_layer_name) + for (_, _, p, _, layer_name) in kvp ): return (virtual_layer_name, kvo, kernel) else: diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index dcb7505ead..86e1aac529 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -161,7 +161,7 @@ def unsatisfied( "TypeError - Too many values provided to list option.", ) return {config_path: self} - if not all([isinstance(element, self.element_type) for element in value]): + if not all(isinstance(element, self.element_type) for element in value): vollog.log( constants.LOGLEVEL_V, "TypeError - At least one element in the list is not of the correct type.", @@ -181,7 +181,7 @@ def __init__(self, choices: List[str], *args, **kwargs) -> None: """ super().__init__(*args, **kwargs) if not isinstance(choices, list) or any( - [not isinstance(choice, str) for choice in choices] + not isinstance(choice, str) for choice in choices ): raise TypeError("ChoiceRequirement takes a list of strings as choices") self.choices = choices @@ -410,11 +410,9 @@ def construct( args = {"context": context, "config_path": config_path, "name": name} if any( - [ - subreq.unsatisfied(context, config_path) - for subreq in self.requirements.values() - if not subreq.optional - ] + subreq.unsatisfied(context, config_path) + for subreq in self.requirements.values() + if not subreq.optional ): return None @@ -485,11 +483,9 @@ def construct( args = {"context": context, "config_path": config_path, "name": name} if any( - [ - subreq.unsatisfied(context, config_path) - for subreq in self.requirements.values() - if not subreq.optional - ] + subreq.unsatisfied(context, config_path) + for subreq in self.requirements.values() + if not subreq.optional ): return None @@ -710,11 +706,9 @@ def construct( args = {"context": context, "config_path": config_path, "name": name} if any( - [ - subreq.unsatisfied(context, config_path) - for subreq in self.requirements.values() - if not subreq.optional - ] + subreq.unsatisfied(context, config_path) + for subreq in self.requirements.values() + if not subreq.optional ): return None diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index ab568b927a..51d25510d2 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -216,7 +216,7 @@ def has_valid_members(self, member_names: List[str]) -> bool: Args: member_names: List of names to test as to members with those names validity """ - return all([self.has_valid_member(member_name) for member_name in member_names]) + return all(self.has_valid_member(member_name) for member_name in member_names) class VolTemplateProxy(metaclass=abc.ABCMeta): """A container for proxied methods that the ObjectTemplate of this diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index b969b952b3..2b0df53728 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -270,10 +270,8 @@ def is_valid(self, offset: int, length: int = 1) -> bool: try: # TODO: Consider reimplementing this, since calls to mapping can call is_valid return all( - [ - self._context.layers[layer].is_valid(mapped_offset) - for _, _, mapped_offset, _, layer in self.mapping(offset, length) - ] + self._context.layers[layer].is_valid(mapped_offset) + for _, _, mapped_offset, _, layer in self.mapping(offset, length) ) except exceptions.InvalidAddressException: return False diff --git a/volatility3/framework/layers/registry.py b/volatility3/framework/layers/registry.py index 9841d2bb08..471d19f68f 100644 --- a/volatility3/framework/layers/registry.py +++ b/volatility3/framework/layers/registry.py @@ -318,10 +318,8 @@ def is_valid(self, offset: int, length: int = 1) -> bool: with contextlib.suppress(exceptions.InvalidAddressException): # Pass this to the lower layers for now return all( - [ - self.context.layers[layer].is_valid(offset, length) - for (_, _, offset, length, layer) in self.mapping(offset, length) - ] + self.context.layers[layer].is_valid(offset, length) + for (_, _, offset, length, layer) in self.mapping(offset, length) ) return False diff --git a/volatility3/framework/layers/segmented.py b/volatility3/framework/layers/segmented.py index e8c0670725..9825ae15cd 100644 --- a/volatility3/framework/layers/segmented.py +++ b/volatility3/framework/layers/segmented.py @@ -51,10 +51,8 @@ def is_valid(self, offset: int, length: int = 1) -> bool: try: base_layer = self._context.layers[self._base_layer] return all( - [ - base_layer.is_valid(mapped_offset) - for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length) - ] + base_layer.is_valid(mapped_offset) + for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length) ) except exceptions.InvalidAddressException: return False diff --git a/volatility3/framework/objects/__init__.py b/volatility3/framework/objects/__init__.py index 316a30becd..b65277067a 100644 --- a/volatility3/framework/objects/__init__.py +++ b/volatility3/framework/objects/__init__.py @@ -928,10 +928,8 @@ def _check_members( members, collections.abc.Mapping ), f"{agg_name} members parameter must be a mapping: {type(members)}" assert all( - [ - (isinstance(member, tuple) and len(member) == 2) - for member in members.values() - ] + (isinstance(member, tuple) and len(member) == 2) + for member in members.values() ), f"{agg_name} members must be a tuple of relative_offsets and templates" def member(self, attr: str = "member") -> object: diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 7fced6acd1..201a443f7d 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -53,9 +53,7 @@ def _check_members(self, var_ops, var_name, members): def _check_afinfo(self, var_name, var, op_members, seq_members): # check if object has a least one of the members used for analysis by this function required_members = ["seq_fops", "seq_ops", "seq_show"] - has_required_member = any( - [var.has_member(member) for member in required_members] - ) + has_required_member = any(var.has_member(member) for member in required_members) if not has_required_member: vollog.debug( f"{var_name} object at {hex(var.vol.offset)} had none of the required members: {', '.join([member for member in required_members])}" diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index d1cb9f4601..c754e43eff 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -66,7 +66,7 @@ def passthrough(name: str, selected: List[str]) -> bool: if selected_list: def filter_plugins(name: str, selected: List[str]) -> bool: - return any([s in name for s in selected]) + return any(s in name for s in selected) filter_func = filter_plugins else: @@ -252,10 +252,8 @@ def run(self): continue if filter_list and not any( - [ - filter in plugin_class.__module__ + "." + plugin_class.__name__ - for filter in filter_list - ] + filter in plugin_class.__module__ + "." + plugin_class.__name__ + for filter in filter_list ): continue diff --git a/volatility3/framework/symbols/metadata.py b/volatility3/framework/symbols/metadata.py index 1498293710..95f542f079 100644 --- a/volatility3/framework/symbols/metadata.py +++ b/volatility3/framework/symbols/metadata.py @@ -24,8 +24,8 @@ def version(self) -> Optional[Tuple[int]]: version = self._json_data.get("version", None) if not version: return None - if all([x in "0123456789." for x in version]): - return tuple([int(x) for x in version.split(".")]) + if all(x in "0123456789." for x in version): + return tuple(int(x) for x in version.split(".")) vollog.log( constants.LOGLEVEL_VVVV, f"Metadata version contains unexpected characters: '{version}'", From a63e9f1699e4042d1372c0873b05f207e2a8a8d4 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 2 Oct 2024 13:00:45 +1000 Subject: [PATCH 149/250] Add --remote-isf-url argument to be able mutually exclusive to --offline --- volatility3/cli/__init__.py | 11 ++++++++++- volatility3/cli/volshell/__init__.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 1209d7cdc7..674fb4b4b2 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -235,12 +235,19 @@ def run(self): default=constants.CACHE_PATH, type=str, ) - parser.add_argument( + isf_group = parser.add_mutually_exclusive_group() + isf_group.add_argument( "--offline", help="Do not search online for additional JSON files", default=False, action="store_true", ) + isf_group.add_argument( + "--remote-isf-url", + help="Search online for ISF json files", + default=constants.REMOTE_ISF_URL, + type=str, + ) parser.add_argument( "--filters", help="List of filters to apply to the output (in the form of [+-]columname,pattern[!])", @@ -313,6 +320,8 @@ def run(self): if partial_args.offline: constants.OFFLINE = partial_args.offline + elif partial_args.remote_isf_url: + constants.REMOTE_ISF_URL = partial_args.remote_isf_url # Do the initialization ctx = contexts.Context() # Construct a blank context diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 2bf1958e2d..fc2f87bc22 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -159,12 +159,19 @@ def run(self): default=constants.CACHE_PATH, type=str, ) - parser.add_argument( + isf_group = parser.add_mutually_exclusive_group() + isf_group.add_argument( "--offline", help="Do not search online for additional JSON files", default=False, action="store_true", ) + isf_group.add_argument( + "--remote-isf-url", + help="Search online for ISF json files", + default=constants.REMOTE_ISF_URL, + type=str, + ) # Volshell specific flags os_specific = parser.add_mutually_exclusive_group(required=False) @@ -236,6 +243,8 @@ def run(self): if partial_args.offline: constants.OFFLINE = partial_args.offline + elif partial_args.remote_isf_url: + constants.REMOTE_ISF_URL = partial_args.remote_isf_url # Do the initialization ctx = contexts.Context() # Construct a blank context From a64813be43594d5171748c901e1c10e2c2271ed0 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 2 Oct 2024 13:08:05 +1000 Subject: [PATCH 150/250] Added metavar and single letter argument to the --remote-isf-url --- volatility3/cli/__init__.py | 2 ++ volatility3/cli/volshell/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 674fb4b4b2..50b5579afd 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -243,7 +243,9 @@ def run(self): action="store_true", ) isf_group.add_argument( + "-u", "--remote-isf-url", + metavar="URL", help="Search online for ISF json files", default=constants.REMOTE_ISF_URL, type=str, diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index fc2f87bc22..5172c5363f 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -167,7 +167,9 @@ def run(self): action="store_true", ) isf_group.add_argument( + "-u", "--remote-isf-url", + metavar="URL", help="Search online for ISF json files", default=constants.REMOTE_ISF_URL, type=str, From 57b5aabdfe885dd054ceb7d1cd1227b46f30ac22 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 3 Oct 2024 11:50:03 +1000 Subject: [PATCH 151/250] Add linux.ptrace plugin to enumerate tracer and tracee tasks --- .../framework/constants/linux/__init__.py | 39 +++++- volatility3/framework/plugins/linux/ptrace.py | 120 ++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 volatility3/framework/plugins/linux/ptrace.py diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 3eabc2341a..0567b8574c 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,7 +5,7 @@ Linux-specific values that aren't found in debug symbols """ -from enum import IntEnum +from enum import IntEnum, Flag KERNEL_NAME = "__kernel__" @@ -302,3 +302,40 @@ class ELF_CLASS(IntEnum): ELFCLASSNONE = 0 ELFCLASS32 = 1 ELFCLASS64 = 2 + + +PT_OPT_FLAG_SHIFT = 3 + +PTRACE_EVENT_FORK = 1 +PTRACE_EVENT_VFORK = 2 +PTRACE_EVENT_CLONE = 3 +PTRACE_EVENT_EXEC = 4 +PTRACE_EVENT_VFORK_DONE = 5 +PTRACE_EVENT_EXIT = 6 +PTRACE_EVENT_SECCOMP = 7 + +PTRACE_O_EXITKILL = 1 << 20 +PTRACE_O_SUSPEND_SECCOMP = 1 << 21 + + +class PT_FLAGS(Flag): + "PTrace flags" + PT_PTRACED = 0x00001 + PT_SEIZED = 0x10000 + + PT_TRACESYSGOOD = 1 << (PT_OPT_FLAG_SHIFT + 0) + PT_TRACE_FORK = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_FORK) + PT_TRACE_VFORK = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_VFORK) + PT_TRACE_CLONE = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_CLONE) + PT_TRACE_EXEC = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_EXEC) + PT_TRACE_VFORK_DONE = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_VFORK_DONE) + PT_TRACE_EXIT = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_EXIT) + PT_TRACE_SECCOMP = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_SECCOMP) + + PT_EXITKILL = PTRACE_O_EXITKILL << PT_OPT_FLAG_SHIFT + PT_SUSPEND_SECCOMP = PTRACE_O_SUSPEND_SECCOMP << PT_OPT_FLAG_SHIFT + + @property + def flags(self) -> str: + """Returns the ptrace flags string""" + return str(self).replace(self.__class__.__name__ + ".", "") diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py new file mode 100644 index 0000000000..793a1b3a1e --- /dev/null +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -0,0 +1,120 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import List + +from volatility3.framework import renderers, interfaces, constants, objects +from volatility3.framework.constants.linux import PT_FLAGS +from volatility3.framework.constants.architectures import LINUX_ARCHS +from volatility3.framework.objects import utility +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class Ptrace(plugins.PluginInterface): + """Enumerates tracer and tracee tasks""" + + _required_framework_version = (2, 10, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=LINUX_ARCHS, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 2, 1) + ), + ] + + @classmethod + def enumerate_ptraced_tasks( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + ): + vmlinux = context.modules[symbol_table] + + tsk_struct_symname = vmlinux.symbol_table_name + constants.BANG + "task_struct" + + tasks = pslist.PsList.list_tasks( + context, + symbol_table, + filter_func=pslist.PsList.create_pid_filter(), + include_threads=True, + ) + + for task in tasks: + tracing_tid_list = [ + int(task_being_traced.pid) + for task_being_traced in task.ptraced.to_list( + tsk_struct_symname, "ptrace_entry" + ) + ] + + if task.ptrace == 0 and not tracing_tid_list: + continue + + flags = ( + PT_FLAGS(task.ptrace).flags + if task.ptrace != 0 + else renderers.NotAvailableValue() + ) + + traced_by_tid = ( + task.parent.pid + if task.real_parent != task.parent + else renderers.NotAvailableValue() + ) + + tracing_tids = ",".join(map(str, tracing_tid_list)) + + yield task.comm, task.tgid, task.pid, traced_by_tid, tracing_tids, flags + + def _generator(self, symbol_table): + for fields in self.enumerate_ptraced_tasks(self.context, symbol_table): + yield (0, fields) + + @staticmethod + def format_fields_with_headers(headers, generator): + """Uses the headers type to cast the fields obtained from the generator""" + for level, fields in generator: + formatted_fields = [] + for header, field in zip(headers, fields): + header_type = header[1] + + if isinstance( + field, (header_type, interfaces.renderers.BaseAbsentValue) + ): + formatted_field = field + elif isinstance(field, objects.Array) and header_type is str: + formatted_field = utility.array_to_string(field) + else: + formatted_field = header_type(field) + + formatted_fields.append(formatted_field) + yield level, formatted_fields + + def run(self): + symbol_table = self.config["kernel"] + + headers = [ + ("Process", str), + ("PID", int), + ("TID", int), + ("Traced by TID", int), + ("Tracing TIDs", str), + ("Flags", str), + ] + return renderers.TreeGrid( + headers, + self.format_fields_with_headers(headers, self._generator(symbol_table)), + ) From 590aa9c19ca56250e87c07ed49dbddc55a22b4a8 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 3 Oct 2024 14:50:39 +1000 Subject: [PATCH 152/250] Make it callable from other plugins. Additionally, classmethod helpers were added, and docstrings were enhanced for improved usability and clarity. --- .../framework/plugins/linux/hidden_modules.py | 154 +++++++++++++----- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 6d04c08afc..4843f30373 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -2,10 +2,9 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import re -import functools import logging import contextlib -from typing import List, Iterable +from typing import List, Set, Tuple, Iterable from volatility3.framework import renderers, interfaces, exceptions, objects from volatility3.framework.constants.architectures import LINUX_ARCHS from volatility3.framework.renderers import format_hints @@ -41,7 +40,21 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ), ] - def _get_modules_memory_boundaries(self, vmlinux): + @staticmethod + def get_modules_memory_boundaries( + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Tuple[int]: + """Determine the boundaries of the module allocation area + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A tuple containing the minimum and maximum addresses for the module allocation area. + """ + vmlinux = context.modules[vmlinux_module_name] if vmlinux.has_symbol("mod_tree"): mod_tree = vmlinux.object_from_symbol("mod_tree") modules_addr_min = mod_tree.addr_min @@ -73,8 +86,8 @@ def _get_modules_memory_boundaries(self, vmlinux): return modules_addr_min, modules_addr_max + @staticmethod def _get_module_state_values_bytes( - self, context: interfaces.context.ContextInterface, vmlinux_module_name: str, ) -> List[bytes]: @@ -97,12 +110,13 @@ def _get_module_state_values_bytes( ] return values_bytes - def get_hidden_modules_vol2( - self, + @classmethod + def _get_hidden_modules_vol2( + cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, - known_module_addresses, - modules_memory_boundaries: tuple, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules using the traditional implementation. @@ -111,6 +125,9 @@ def get_hidden_modules_vol2( Args: context: The context to retrieve required elements (layers, symbol tables) from vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: module objects """ @@ -175,7 +192,7 @@ def get_hidden_modules_vol2( scan_buf = b"".join(scan_list) del scan_list - module_state_values_bytes = self._get_module_state_values_bytes( + module_state_values_bytes = cls._get_module_state_values_bytes( context, vmlinux_module_name ) values_bytes_pattern = b"|".join(module_state_values_bytes) @@ -190,27 +207,37 @@ def get_hidden_modules_vol2( if module and module.is_valid(): yield module - @functools.cached_property - def module_address_alignment(self) -> int: + @classmethod + def _get_module_address_alignment( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> int: """Obtain the module memory address alignment. This is only used with the fast scan method. struct module is aligned to the L1 cache line, which is typically 64 bytes for most common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this will still work. + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + Returns: The struct module alignment """ # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata - # The cached_property won't provide any benefits until then + # Also, 'context' and 'vmlinux_module_name' are not used yet, but they will be needed to obtain + # the type metadata return 64 - def get_hidden_modules_fast( - self, + @classmethod + def _get_hidden_modules_fast( + cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, - known_module_addresses, - modules_memory_boundaries: tuple, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules by taking advantage of memory address alignment patterns @@ -229,6 +256,9 @@ def get_hidden_modules_fast( Args: context: The context to retrieve required elements (layers, symbol tables) from vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: module objects """ @@ -237,12 +267,16 @@ def get_hidden_modules_fast( module_addr_min, module_addr_max = modules_memory_boundaries - module_state_values_bytes = self._get_module_state_values_bytes( + module_state_values_bytes = cls._get_module_state_values_bytes( + context, vmlinux_module_name + ) + + module_address_alignment = cls._get_module_address_alignment( context, vmlinux_module_name ) for module_addr in range( - module_addr_min, module_addr_max, self.module_address_alignment + module_addr_min, module_addr_max, module_address_alignment ): if module_addr in known_module_addresses: continue @@ -264,51 +298,59 @@ def get_hidden_modules_fast( if module and module.is_valid(): yield module - def _validate_alignment_patterns(self, addresses: Iterable[int]) -> bool: + @staticmethod + def _validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: """Check if the memory addresses meet our alignments patterns Args: addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation Returns: True if all the addresses meet the alignment """ - return all(addr % self.module_address_alignment == 0 for addr in addresses) + return all(addr % address_alignment == 0 for addr in addresses) + @classmethod def get_hidden_modules( - self, + cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, + fast_method: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules Args: context: The context to retrieve required elements (layers, symbol tables) from vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + fast_method: If True, it uses the fast method. Otherwise, it uses the traditional one. + Yields: module objects """ - vmlinux = context.modules[vmlinux_module_name] - vmlinux_layer = context.layers[vmlinux.layer_name] - - known_module_addresses = { - vmlinux_layer.canonicalize(module.vol.offset) - for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) - } - - modules_memory_boundaries = self._get_modules_memory_boundaries(vmlinux) - - if self.config.get("fast"): - if self._validate_alignment_patterns(known_module_addresses): - scan_method = self.get_hidden_modules_fast + if fast_method: + module_address_alignment = cls._get_module_address_alignment( + context, vmlinux_module_name + ) + if cls._validate_alignment_patterns( + known_module_addresses, module_address_alignment + ): + scan_method = cls._get_hidden_modules_fast else: vollog.warning( - f"Module addresses aren't aligned to {self.module_address_alignment} bytes. " + f"Module addresses aren't aligned to {module_address_alignment} bytes. " "Switching to the traditional scan method." ) - scan_method = self.get_hidden_modules_vol2 + scan_method = cls._get_hidden_modules_vol2 else: - scan_method = self.get_hidden_modules_vol2 + scan_method = cls._get_hidden_modules_vol2 yield from scan_method( context, @@ -317,9 +359,45 @@ def get_hidden_modules( modules_memory_boundaries, ) + @classmethod + def get_lsmod_module_addresses( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Set[int]: + """Obtain a set the known module addresses from linux.lsmod plugin + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A set containing known kernel module addresses + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + known_module_addresses = { + vmlinux_layer.canonicalize(module.vol.offset) + for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) + } + return known_module_addresses + def _generator(self): vmlinux_module_name = self.config["kernel"] - for module in self.get_hidden_modules(self.context, vmlinux_module_name): + known_module_addresses = self.get_lsmod_module_addresses( + self.context, vmlinux_module_name + ) + modules_memory_boundaries = self.get_modules_memory_boundaries( + self.context, vmlinux_module_name + ) + for module in self.get_hidden_modules( + self.context, + vmlinux_module_name, + known_module_addresses, + modules_memory_boundaries, + fast_method=self.config.get("fast"), + ): module_addr = module.vol.offset module_name = module.get_name() or renderers.NotAvailableValue() fields = (format_hints.Hex(module_addr), module_name) From 8d925bd8ad6346370d6256ddacced9d0db36b23d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 3 Oct 2024 15:17:02 +1000 Subject: [PATCH 153/250] Added the --heuristic-mode option, which relaxes constraints to improve detection of more advanced threats --- .../framework/plugins/linux/hidden_modules.py | 46 ++++++++++++------- .../symbols/linux/extensions/__init__.py | 4 +- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 4843f30373..1ec43aefdb 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -38,6 +38,13 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] optional=True, default=False, ), + requirements.BooleanRequirement( + name="heuristic-mode", + description="Relaxed constraints. This may generate some false positives and" + "take a bit longer. This feature is available only when using the --fast option", + optional=True, + default=False, + ), ] @staticmethod @@ -117,6 +124,7 @@ def _get_hidden_modules_vol2( vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, + heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules using the traditional implementation. @@ -127,6 +135,7 @@ def _get_hidden_modules_vol2( vmlinux_module_name: The name of the kernel module on which to operate known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + heuristic_mode: ignored for this scan method. Yields: module objects @@ -227,8 +236,8 @@ def _get_module_address_alignment( The struct module alignment """ # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata - # Also, 'context' and 'vmlinux_module_name' are not used yet, but they will be needed to obtain - # the type metadata + # Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be + # essential for retrieving type metadata in the future. return 64 @classmethod @@ -238,6 +247,7 @@ def _get_hidden_modules_fast( vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, + heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules by taking advantage of memory address alignment patterns @@ -258,7 +268,7 @@ def _get_hidden_modules_fast( vmlinux_module_name: The name of the kernel module on which to operate known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - + heuristic_mode: If True, it loosens constraints to enhance the detection of advanced threats. Yields: module objects """ @@ -281,21 +291,22 @@ def _get_hidden_modules_fast( if module_addr in known_module_addresses: continue - try: - # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() - module_state_bytes = vmlinux_layer.read( - module_addr, len(module_state_values_bytes[0]) - ) - if module_state_bytes not in module_state_values_bytes: + if not heuristic_mode: + try: + # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() + module_state_bytes = vmlinux_layer.read( + module_addr, len(module_state_values_bytes[0]) + ) + if module_state_bytes not in module_state_values_bytes: + continue + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): continue - except ( - exceptions.PagedInvalidAddressException, - exceptions.InvalidAddressException, - ): - continue module = vmlinux.object("module", offset=module_addr, absolute=True) - if module and module.is_valid(): + if module and module.is_valid(strict_states=not heuristic_mode): yield module @staticmethod @@ -322,6 +333,7 @@ def get_hidden_modules( known_module_addresses: Set[int], modules_memory_boundaries: Tuple, fast_method: bool = False, + heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules @@ -331,7 +343,7 @@ def get_hidden_modules( known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. fast_method: If True, it uses the fast method. Otherwise, it uses the traditional one. - + heuristic_mode: If True, it loosens constraints to enhance the detection of advanced threats. Yields: module objects """ @@ -357,6 +369,7 @@ def get_hidden_modules( vmlinux_module_name, known_module_addresses, modules_memory_boundaries, + heuristic_mode, ) @classmethod @@ -397,6 +410,7 @@ def _generator(self): known_module_addresses, modules_memory_boundaries, fast_method=self.config.get("fast"), + heuristic_mode=self.config.get("heuristic-mode"), ): module_addr = module.vol.offset module_name = module.get_name() or renderers.NotAvailableValue() diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b30634ca3e..810df41f80 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -35,13 +35,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization - def is_valid(self): + def is_valid(self, strict_states=True): layer = self._context.layers[self.vol.layer_name] # Make sure the entire module content is readable if not layer.is_valid(self.vol.offset, self.vol.size): return False - if not self.state.is_valid_choice: + if strict_states and not self.state.is_valid_choice: return False core_size = self.get_core_size() From e8754fadbd93864ea2623e7df10bb33265db5073 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 3 Oct 2024 15:50:30 +1000 Subject: [PATCH 154/250] Fix typo in usage help --- volatility3/framework/plugins/linux/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 1ec43aefdb..86a4c36261 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -40,7 +40,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ), requirements.BooleanRequirement( name="heuristic-mode", - description="Relaxed constraints. This may generate some false positives and" + description="Relaxed constraints. This may generate false positives and " "take a bit longer. This feature is available only when using the --fast option", optional=True, default=False, From 4ac9b0825a6a6b7548dfcb36cc2681c9b4753f3c Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 3 Oct 2024 10:49:38 -0500 Subject: [PATCH 155/250] Linux: Properly get paths in simple_dname paths Some dentry paths were not being recovered from `simple_dname` paths, this fixes the issue by properly dereferencing the string pointer. --- volatility3/framework/symbols/linux/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 2410d2627b..1aceade242 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -206,7 +206,14 @@ def _get_new_sock_pipe_path(cls, context, task, filp) -> str: pre_name = "pipe" elif sym == "simple_dname": - pre_name = cls._get_path_file(task, filp) + name = dentry.d_name.name + if name: + pre_name = name.dereference().cast( + "string", max_length=255, errors="replace" + ) + return "/" + pre_name + " (deleted)" + else: + pre_name = "" elif sym == "ns_dname": # From Kernels 3.19 From e605bee4119e5c7da857ce9576520c36a7eb4b19 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 6 Sep 2024 14:07:24 -0500 Subject: [PATCH 156/250] Windows: unloadedmodules bugfix This fixes a bug in the `windows.unloadedmodules` plugin. An `InvalidAddressException` can be raised when parsing the module name; this adds a try/except block to replace the missing module name with a `renderers.UnreadableValue` when the exception occurs. --- volatility3/framework/plugins/windows/unloadedmodules.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/unloadedmodules.py b/volatility3/framework/plugins/windows/unloadedmodules.py index 0d88e96ffb..01e5758189 100644 --- a/volatility3/framework/plugins/windows/unloadedmodules.py +++ b/volatility3/framework/plugins/windows/unloadedmodules.py @@ -7,7 +7,7 @@ from typing import List, Iterable from volatility3.framework import constants -from volatility3.framework import interfaces, symbols +from volatility3.framework import interfaces, symbols, exceptions from volatility3.framework import renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import configuration @@ -132,10 +132,15 @@ def _generator(self): kernel.symbol_table_name, unloadedmodule_table_name, ): + try: + name = mod.Name.String + except exceptions.InvalidAddressException: + name = renderers.UnreadableValue() + yield ( 0, ( - mod.Name.String, + name, format_hints.Hex(mod.StartAddress), format_hints.Hex(mod.EndAddress), conversion.wintime_to_datetime(mod.CurrentTime), From b5948d7ec0e9bb6dcc193f8c74b40bdb498ac3a8 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 4 Oct 2024 07:48:25 +1000 Subject: [PATCH 157/250] Linux: hidden_modules: Add @Abyss-W4tcher suggestion to optimize the fast scan method for even better performance, using the mkobj.mod self referential validation used in module.is_valid() as pre-filter Removed the --heuristic-mode and the module.states validation, since the self referential check is enough by itself --- .../framework/plugins/linux/hidden_modules.py | 53 ++++++++----------- .../symbols/linux/extensions/__init__.py | 20 +++---- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 86a4c36261..a76d1c69d7 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -38,13 +38,6 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] optional=True, default=False, ), - requirements.BooleanRequirement( - name="heuristic-mode", - description="Relaxed constraints. This may generate false positives and " - "take a bit longer. This feature is available only when using the --fast option", - optional=True, - default=False, - ), ] @staticmethod @@ -124,7 +117,6 @@ def _get_hidden_modules_vol2( vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, - heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules using the traditional implementation. @@ -135,7 +127,6 @@ def _get_hidden_modules_vol2( vmlinux_module_name: The name of the kernel module on which to operate known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - heuristic_mode: ignored for this scan method. Yields: module objects @@ -247,7 +238,6 @@ def _get_hidden_modules_fast( vmlinux_module_name: str, known_module_addresses: Set[int], modules_memory_boundaries: Tuple, - heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules by taking advantage of memory address alignment patterns @@ -268,7 +258,6 @@ def _get_hidden_modules_fast( vmlinux_module_name: The name of the kernel module on which to operate known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - heuristic_mode: If True, it loosens constraints to enhance the detection of advanced threats. Yields: module objects """ @@ -276,37 +265,41 @@ def _get_hidden_modules_fast( vmlinux_layer = context.layers[vmlinux.layer_name] module_addr_min, module_addr_max = modules_memory_boundaries - - module_state_values_bytes = cls._get_module_state_values_bytes( - context, vmlinux_module_name - ) - module_address_alignment = cls._get_module_address_alignment( context, vmlinux_module_name ) + mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") + mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") + offset_to_mkobj_mod = mkobj_offset + mod_offset + mod_member_template = vmlinux.get_type("module_kobject").vol.members["mod"][1] + mod_size = mod_member_template.size + mod_member_data_format = mod_member_template.data_format + for module_addr in range( module_addr_min, module_addr_max, module_address_alignment ): if module_addr in known_module_addresses: continue - if not heuristic_mode: - try: - # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() - module_state_bytes = vmlinux_layer.read( - module_addr, len(module_state_values_bytes[0]) - ) - if module_state_bytes not in module_state_values_bytes: - continue - except ( - exceptions.PagedInvalidAddressException, - exceptions.InvalidAddressException, - ): + try: + # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() + self_referential_bytes = vmlinux_layer.read( + module_addr + offset_to_mkobj_mod, mod_size + ) + self_referential = objects.convert_data_to_value( + self_referential_bytes, int, mod_member_data_format + ) + if self_referential != module_addr: continue + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): + continue module = vmlinux.object("module", offset=module_addr, absolute=True) - if module and module.is_valid(strict_states=not heuristic_mode): + if module and module.is_valid(): yield module @staticmethod @@ -369,7 +362,6 @@ def get_hidden_modules( vmlinux_module_name, known_module_addresses, modules_memory_boundaries, - heuristic_mode, ) @classmethod @@ -410,7 +402,6 @@ def _generator(self): known_module_addresses, modules_memory_boundaries, fast_method=self.config.get("fast"), - heuristic_mode=self.config.get("heuristic-mode"), ): module_addr = module.vol.offset module_name = module.get_name() or renderers.NotAvailableValue() diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 810df41f80..9f66220019 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -35,15 +35,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization - def is_valid(self, strict_states=True): + def is_valid(self): layer = self._context.layers[self.vol.layer_name] # Make sure the entire module content is readable if not layer.is_valid(self.vol.offset, self.vol.size): return False - if strict_states and not self.state.is_valid_choice: - return False - core_size = self.get_core_size() if not ( 1 <= core_size <= 20000000 @@ -52,14 +49,13 @@ def is_valid(self, strict_states=True): ): return False - if self.has_member("mkobj") and self.mkobj.has_member("mod"): - if not ( - self.mkobj - and self.mkobj.mod - and self.mkobj.mod.is_readable() - and self.mkobj.mod == self.vol.offset - ): - return False + if not ( + self.mkobj + and self.mkobj.mod + and self.mkobj.mod.is_readable() + and self.mkobj.mod == self.vol.offset + ): + return False return True From f455c30e4962789e5cac595f57e9ea5090fac6b6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 4 Oct 2024 09:51:08 +1000 Subject: [PATCH 158/250] Linux: hidden_modules: remove missed optional heuristic_mode argument --- volatility3/framework/plugins/linux/hidden_modules.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index a76d1c69d7..6542f58f3a 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -326,7 +326,6 @@ def get_hidden_modules( known_module_addresses: Set[int], modules_memory_boundaries: Tuple, fast_method: bool = False, - heuristic_mode: bool = False, ) -> Iterable[interfaces.objects.ObjectInterface]: """Enumerate hidden modules @@ -336,7 +335,6 @@ def get_hidden_modules( known_module_addresses: Set with known module addresses modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. fast_method: If True, it uses the fast method. Otherwise, it uses the traditional one. - heuristic_mode: If True, it loosens constraints to enhance the detection of advanced threats. Yields: module objects """ From cffad872bbae6fda31eb415de230e445603c451c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 4 Oct 2024 12:22:53 +1000 Subject: [PATCH 159/250] Linux: sockstat. Fix #1271 which unnecessarily extends the interface of the list_sockets() class method --- volatility3/framework/plugins/linux/sockstat.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index bb9c46fad6..0ddd3e26d6 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -22,7 +22,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface): _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (3, 0, 0) def __init__(self, vmlinux, task, *args, **kwargs): super().__init__(*args, **kwargs) @@ -439,7 +439,7 @@ class Sockstat(plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 0, 0) + _version = (3, 0, 0) @classmethod def get_requirements(cls): @@ -450,7 +450,7 @@ def get_requirements(cls): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="SockHandlers", component=SockHandlers, version=(2, 0, 0) + name="SockHandlers", component=SockHandlers, version=(3, 0, 0) ), requirements.PluginRequirement( name="lsof", plugin=lsof.Lsof, version=(2, 0, 0) @@ -550,7 +550,7 @@ def list_sockets( except AttributeError: netns_id = NotAvailableValue() - yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields + yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields def _format_fields(self, sock_stat, protocol): """Prepare the socket fields to be rendered @@ -597,7 +597,6 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): ) for ( - task_comm, task, netns_id, fd_num, @@ -618,6 +617,8 @@ def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): else NotAvailableValue() ) + task_comm = utility.array_to_string(task.comm) + fields = ( netns_id, task_comm, From bb5a4475028c95af0e9d1e583d749ac9d01b82f3 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Sat, 5 Oct 2024 09:58:01 -0500 Subject: [PATCH 160/250] #1292 - use correct sector size for DataSectionObject file dumping --- .../framework/symbols/windows/extensions/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 80d3b48b76..ecfc2f1632 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1247,6 +1247,15 @@ def get_available_pages(self) -> Iterable[Tuple[int, int, int]]: is_64bit = symbols.symbol_table_is_64bit(self._context, symbol_table_name) is_pae = self._context.layers[self.vol.layer_name].metadata.get("pae", False) + # the sector_size is used as a multiplier to the StartingSector + # within each _SUBSECTION. ImageSectionObjects use a multiplier + # of 0x200 corresponding to sector alignment on disk, + # while DataSectionObjects use a multiplier of 0x1000 corresponding + # to the size of a page + sector_size = 0x200 + if self.u.Flags.Image != 1: + sector_size = 0x1000 + # This is a null-terminated single-linked list. while subsection != 0: try: @@ -1257,7 +1266,7 @@ def get_available_pages(self) -> Iterable[Tuple[int, int, int]]: # The offset into the file is stored implicitly based on the PTE location within the Subsection. starting_sector = subsection.StartingSector - subsection_offset = starting_sector * 0x200 + subsection_offset = starting_sector * sector_size # Similar to the check in is_valid(), make sure the SubsectionBase is not page aligned. # if subsection.SubsectionBase & self.PAGE_MASK == 0: From 5d072864d726242e85e632edb99449cafaf187dd Mon Sep 17 00:00:00 2001 From: eve Date: Mon, 7 Oct 2024 07:00:05 +0100 Subject: [PATCH 161/250] Docs: Add warning around LiME raw format --- doc/source/getting-started-linux-tutorial.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 26ad2c2e4e..d4b40d0532 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -11,6 +11,7 @@ Volatility3 does not provide the ability to acquire memory. Below are some exam * `AVML - Acquire Volatile Memory for Linux `_ * `LiME - Linux Memory Extract `_ +Be aware that LiME raw format is not supported by volatility3, the padded or lime option should be used instead. `This issue contains further information `_. Procedure to create symbol tables for linux -------------------------------------------- From bf027236950d549b2309159d8d7e7660bd84452e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 7 Oct 2024 17:59:55 +1100 Subject: [PATCH 162/250] Linux: lsof plugin: Fix module import to stick to the style guide --- volatility3/framework/plugins/linux/lsof.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 527815738f..18e67cac9e 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from datetime import datetime +import datetime from dataclasses import dataclass, astuple, field from typing import List, Callable, Tuple @@ -32,9 +32,9 @@ class FDUser: inode_num: int = field(default=renderers.NotAvailableValue()) inode_type: str = field(default=renderers.NotAvailableValue()) file_mode: str = field(default=renderers.NotAvailableValue()) - change_time: datetime = field(default=renderers.NotAvailableValue()) - modification_time: datetime = field(default=renderers.NotAvailableValue()) - access_time: datetime = field(default=renderers.NotAvailableValue()) + change_time: datetime.datetime = field(default=renderers.NotAvailableValue()) + modification_time: datetime.datetime = field(default=renderers.NotAvailableValue()) + access_time: datetime.datetime = field(default=renderers.NotAvailableValue()) inode_size: int = field(default=renderers.NotAvailableValue()) @@ -184,9 +184,9 @@ def run(self): ("Inode", int), ("Type", str), ("Mode", str), - ("Changed", datetime), - ("Modified", datetime), - ("Accessed", datetime), + ("Changed", datetime.datetime), + ("Modified", datetime.datetime), + ("Accessed", datetime.datetime), ("Size", int), ] return renderers.TreeGrid( From bf19b9f2e00de4bb01ea3156efb261b5619cac43 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 7 Oct 2024 18:46:18 +1100 Subject: [PATCH 163/250] Linux: lsof plugin: Fix dataclasses import to stick to the style guide --- volatility3/framework/plugins/linux/lsof.py | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 18e67cac9e..8c3c4a2997 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -3,7 +3,7 @@ # import logging import datetime -from dataclasses import dataclass, astuple, field +import dataclasses from typing import List, Callable, Tuple from volatility3.framework import renderers, interfaces, constants @@ -17,7 +17,7 @@ vollog = logging.getLogger(__name__) -@dataclass +@dataclasses.dataclass class FDUser: """FD user representation, featuring augmented information and formatted fields. This is the data the plugin will eventually display. @@ -28,17 +28,23 @@ class FDUser: task_comm: str fd_num: int full_path: str - device: str = field(default=renderers.NotAvailableValue()) - inode_num: int = field(default=renderers.NotAvailableValue()) - inode_type: str = field(default=renderers.NotAvailableValue()) - file_mode: str = field(default=renderers.NotAvailableValue()) - change_time: datetime.datetime = field(default=renderers.NotAvailableValue()) - modification_time: datetime.datetime = field(default=renderers.NotAvailableValue()) - access_time: datetime.datetime = field(default=renderers.NotAvailableValue()) - inode_size: int = field(default=renderers.NotAvailableValue()) - - -@dataclass + device: str = dataclasses.field(default=renderers.NotAvailableValue()) + inode_num: int = dataclasses.field(default=renderers.NotAvailableValue()) + inode_type: str = dataclasses.field(default=renderers.NotAvailableValue()) + file_mode: str = dataclasses.field(default=renderers.NotAvailableValue()) + change_time: datetime.datetime = dataclasses.field( + default=renderers.NotAvailableValue() + ) + modification_time: datetime.datetime = dataclasses.field( + default=renderers.NotAvailableValue() + ) + access_time: datetime.datetime = dataclasses.field( + default=renderers.NotAvailableValue() + ) + inode_size: int = dataclasses.field(default=renderers.NotAvailableValue()) + + +@dataclasses.dataclass class FDInternal: """FD internal representation containing only the core objects @@ -168,7 +174,7 @@ def _generator(self, pids, vmlinux_module_name): self.context, vmlinux_module_name, filter_func=filter_func ): fd_user = fd_internal.to_user() - yield (0, astuple(fd_user)) + yield (0, dataclasses.astuple(fd_user)) def run(self): pids = self.config.get("pid", None) From 2859d7a641bee72f976e2e16b6dfec61bac107ce Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 7 Oct 2024 18:49:38 +1100 Subject: [PATCH 164/250] Linux: lsof plugin: Fix list_fds() return typing --- volatility3/framework/plugins/linux/lsof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 8c3c4a2997..075f2db848 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -4,7 +4,7 @@ import logging import datetime import dataclasses -from typing import List, Callable, Tuple +from typing import List, Callable, Tuple, Iterable from volatility3.framework import renderers, interfaces, constants from volatility3.framework.configuration import requirements @@ -140,7 +140,7 @@ def list_fds( context: interfaces.context.ContextInterface, vmlinux_module_name: str, filter_func: Callable[[int], bool] = lambda _: False, - ) -> FDInternal: + ) -> Iterable[FDInternal]: """Enumerates open file descriptors in tasks Args: From ee759643456869c5d18cd093f5bb81c138a33eb5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 7 Oct 2024 20:46:44 +1100 Subject: [PATCH 165/250] Linux: lsof plugin: Fix typo in docstring --- volatility3/framework/plugins/linux/lsof.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 075f2db848..42b447dfb9 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -49,7 +49,7 @@ class FDInternal: """FD internal representation containing only the core objects Fields: - task: 'task_truct' object + task: 'task_struct' object fd_fields: FD fields as obtained from LinuxUtilities.files_descriptors_for_process() """ From cb98a085d74531105a18b7974fc6250870955b5a Mon Sep 17 00:00:00 2001 From: MHL Date: Mon, 7 Oct 2024 09:11:49 -0500 Subject: [PATCH 166/250] refs #1296 sort volshell methods alphabetically in hh() output --- volatility3/cli/volshell/generic.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index c5eac36e22..c1ca7b5fa7 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -112,7 +112,7 @@ def help(self, *args): variables = [] print("\nMethods:") - for aliases, item in self.construct_locals(): + for aliases, item in sorted(self.construct_locals()): name = ", ".join(aliases) if item.__doc__ and callable(item): print(f"* {name}") @@ -125,8 +125,7 @@ def help(self, *args): print(f" {var}") def construct_locals(self) -> List[Tuple[List[str], Any]]: - """Returns a dictionary listing the functions to be added to the - environment.""" + """Returns a listing of the functions to be added to the environment.""" return [ (["dt", "display_type"], self.display_type), (["db", "display_bytes"], self.display_bytes), From 0dd082b7fda2d762026a823b38ba004819b039be Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Mon, 7 Oct 2024 12:26:17 -0500 Subject: [PATCH 167/250] #816 initial PR comment fixes --- .../framework/plugins/windows/cmdscan.py | 43 +++--- .../framework/plugins/windows/consoles.py | 6 +- .../consoles-win10-17763-3232-x64.json | 4 +- .../consoles/consoles-win10-17763-x64.json | 4 +- .../consoles/consoles-win10-18362-x64.json | 4 +- .../consoles/consoles-win10-19041-x64.json | 4 +- .../consoles-win10-20348-1970-x64.json | 47 +----- .../consoles-win10-20348-2461-x64.json | 47 +----- .../consoles/consoles-win10-20348-x64.json | 47 +----- .../consoles/consoles-win10-22000-x64.json | 47 +----- ...son => consoles-win10-22621-3527-x64.json} | 47 +----- .../consoles/consoles-win10-22621-x64.json | 47 +----- .../consoles/consoles-win10-25398-x64.json | 47 +----- .../symbols/windows/extensions/consoles.py | 144 +++++++----------- 14 files changed, 111 insertions(+), 427 deletions(-) rename volatility3/framework/symbols/windows/consoles/{consoles-win10-22621-3672-x64.json => consoles-win10-22621-3527-x64.json} (93%) diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 09cb0f3163..9a3460a208 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -61,7 +61,7 @@ def get_filtered_vads( cls, conhost_proc: interfaces.context.ContextInterface, size_filter: Optional[int] = 0x40000000, - ) -> List[Tuple[int, int]]: + ) -> Generator[Tuple[int, int], None, None]: """ Returns vads of a process with size smaller than size_filter @@ -73,20 +73,17 @@ def get_filtered_vads( vad_base: the base address vad_size: the size of the VAD """ - vads = [] for vad in conhost_proc.get_vad_root().traverse(): base = vad.get_start() if vad.get_size() < size_filter: - vads.append((base, vad.get_size())) - - return vads + yield (base, vad.get_size()) @classmethod def get_command_history( cls, context: interfaces.context.ContextInterface, kernel_layer_name: str, - kernel_table_name: str, + kernel_symbol_table_name: str, config_path: str, procs: Generator[interfaces.objects.ObjectInterface, None, None], max_history: Set[int], @@ -100,7 +97,7 @@ def get_command_history( Args: context: The context to retrieve required elements (layers, symbol tables) from kernel_layer_name: The name of the layer on which to operate - kernel_table_name: The name of the table containing the kernel symbols + kernel_symbol_table_name: The name of the table containing the kernel symbols config_path: The config path where to find symbol files procs: list of process objects max_history: an initial set of CommandHistorySize values @@ -138,7 +135,7 @@ def get_command_history( conhost_symbol_table = consoles.Consoles.create_conhost_symbol_table( context, kernel_layer_name, - kernel_table_name, + kernel_symbol_table_name, config_path, proc_layer_name, conhostexe_base, @@ -147,6 +144,9 @@ def get_command_history( conhost_module = context.module( conhost_symbol_table, proc_layer_name, offset=conhostexe_base ) + command_count_max_offset = conhost_module.get_type( + "_COMMAND_HISTORY" + ).relative_child_offset("CommandCountMax") sections = cls.get_filtered_vads(conhost_proc) found_history_for_proc = False @@ -161,15 +161,13 @@ def get_command_history( scanners.BytesScanner(max_history_bytes), sections=sections, ): + command_history = None command_history_properties = [] try: command_history = conhost_module.object( "_COMMAND_HISTORY", - offset=address - - conhost_module.get_type( - "_COMMAND_HISTORY" - ).relative_child_offset("CommandCountMax"), + offset=address - command_count_max_offset, absolute=True, ) @@ -184,13 +182,13 @@ def get_command_history( "level": 0, "name": "_COMMAND_HISTORY", "address": command_history.vol.offset, - "data": "", + "data": None, } ) command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.Application", + "name": "_COMMAND_HISTORY.Application", "address": command_history.Application.vol.offset, "data": command_history.get_application(), } @@ -198,7 +196,7 @@ def get_command_history( command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.ProcessHandle", + "name": "_COMMAND_HISTORY.ProcessHandle", "address": command_history.ConsoleProcessHandle.ProcessHandle.vol.offset, "data": hex( command_history.ConsoleProcessHandle.ProcessHandle @@ -208,7 +206,7 @@ def get_command_history( command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.CommandCount", + "name": "_COMMAND_HISTORY.CommandCount", "address": None, "data": command_history.CommandCount, } @@ -216,7 +214,7 @@ def get_command_history( command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.LastDisplayed", + "name": "_COMMAND_HISTORY.LastDisplayed", "address": command_history.LastDisplayed.vol.offset, "data": command_history.LastDisplayed, } @@ -224,7 +222,7 @@ def get_command_history( command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.CommandCountMax", + "name": "_COMMAND_HISTORY.CommandCountMax", "address": command_history.CommandCountMax.vol.offset, "data": command_history.CommandCountMax, } @@ -233,7 +231,7 @@ def get_command_history( command_history_properties.append( { "level": 1, - "name": f"_COMMAND_HISTORY.CommandBucket", + "name": "_COMMAND_HISTORY.CommandBucket", "address": command_history.CommandBucket.vol.offset, "data": "", } @@ -248,7 +246,7 @@ def get_command_history( "level": 2, "name": f"_COMMAND_HISTORY.CommandBucket_Command_{cmd_index}", "address": bucket_cmd.vol.offset, - "data": bucket_cmd.get_command(), + "data": bucket_cmd.get_command_string(), } ) except Exception as e: @@ -264,6 +262,9 @@ def get_command_history( found_history_for_proc = True yield conhost_proc, command_history, command_history_properties + # if found_history_for_proc is still False, then none of the scanned locations found + # a valid _COMMAND_HISTORY for the process, so yield the process and some empty data + # so the process can at least be reported that it was found with no history if not found_history_for_proc: yield conhost_proc, command_history or None, [] @@ -349,7 +350,7 @@ def _generator( if proc is None: vollog.warn("No conhost.exe processes found.") - def _conhost_proc_filter(self, proc): + def _conhost_proc_filter(self, proc: interfaces.objects.ObjectInterface): """ Used to filter to only conhost.exe processes """ diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index dfc14f2d6d..cef7cba465 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -209,7 +209,7 @@ def determine_conhost_version( (10, 0, 20348, 2520): "consoles-win10-20348-2461-x64", (10, 0, 22000, 0): "consoles-win10-22000-x64", (10, 0, 22621, 1): "consoles-win10-22621-x64", - (10, 0, 22621, 3672): "consoles-win10-22621-3672-x64", + (10, 0, 22621, 3527): "consoles-win10-22621-3527-x64", (10, 0, 25398, 0): "consoles-win10-22000-x64", } @@ -706,7 +706,7 @@ def get_console_info( "level": 3, "name": f"_CONSOLE_INFORMATION.HistoryList.CommandHistory_{index}_Command_{cmd_index}", "address": bucket_cmd.vol.offset, - "data": bucket_cmd.get_command(), + "data": bucket_cmd.get_command_string(), } ) except Exception as e: @@ -918,7 +918,7 @@ def _generator( if proc is None: vollog.warn("No conhost.exe processes found.") - def _conhost_proc_filter(self, proc): + def _conhost_proc_filter(self, proc: interfaces.objects.ObjectInterface) -> bool: """ Used to filter to only conhost.exe processes """ 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 413b6f4663..13382c7ee5 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 @@ -632,14 +632,14 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } 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 b3bf9666c6..f813691bc5 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-17763-x64.json @@ -632,14 +632,14 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } 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 d962418d32..85ef9d7180 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-18362-x64.json @@ -622,14 +622,14 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } 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 0963afee33..8f6dd8c6a8 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-19041-x64.json @@ -622,14 +622,14 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } 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 cee78e2b5e..8357ece4af 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 @@ -598,7 +598,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -628,61 +628,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { 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 59e9fd3b97..f80a991480 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 @@ -598,7 +598,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -628,61 +628,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { 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 df02169cbf..5c7fc84733 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-20348-x64.json @@ -598,7 +598,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -628,61 +628,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json index 0dfaf34af4..cf38d3e14c 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22000-x64.json @@ -624,7 +624,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -654,61 +654,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3527-x64.json similarity index 93% rename from volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json rename to volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3527-x64.json index 4c71ee32e4..a5bda0f350 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3672-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-3527-x64.json @@ -624,7 +624,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -654,61 +654,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json index d6da72ef2a..44f926a87a 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-22621-x64.json @@ -624,7 +624,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -654,61 +654,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json b/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json index 120ad85ecb..5e91d88247 100644 --- a/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json +++ b/volatility3/framework/symbols/windows/consoles/consoles-win10-25398-x64.json @@ -625,7 +625,7 @@ "ExeName": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, @@ -655,61 +655,20 @@ "Source": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 16 }, "Target": { "type": { "kind": "struct", - "name": "_ALIAS_STRING" + "name": "_COMMAND" }, "offset": 48 } }, "kind": "struct", "size": 32 - }, - "_ALIAS_STRING": { - "fields": { - "Chars": { - "type": { - "count": 1, - "kind": "array", - "subtype": { - "kind": "base", - "name": "unsigned char" - } - }, - "offset": 0 - }, - "Pointer": { - "type": { - "kind": "pointer", - "subtype": { - "kind": "base", - "name": "string" - } - }, - "offset": 0 - }, - "Length": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 16 - }, - "Allocated": { - "type": { - "kind": "base", - "name": "unsigned int" - }, - "offset": 24 - } - }, - "kind": "struct", - "size": 32 } }, "metadata": { diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 390b6858dd..96f7892a52 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -2,17 +2,21 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from volatility3.framework import objects +import logging +from typing import Generator, List, Union, Tuple +from volatility3.framework import objects, interfaces from volatility3.framework import constants +vollog = logging.getLogger(__name__) + class ROW(objects.StructType): """A Row Structure.""" - def _valid_dbcs(self, c, n): + def _valid_dbcs(self, dbcs_attr: int, text_attr_msb: int) -> bool: # TODO this need more research and testing # https://github.com/search?q=repo%3Amicrosoft%2Fterminal+DbcsAttr&type=code - valid = n == 0 and c in ( + valid = text_attr_msb == 0 and dbcs_attr in ( 0x0, 0x1, 0x2, @@ -43,11 +47,11 @@ def _valid_dbcs(self, c, n): 0xF0, 0xA0, ) - if n == 0 and not valid: - print("Bad Dbcs Attribute {}".format(hex(c))) + if text_attr_msb == 0 and not valid: + vollog.debug(f"Bad Dbcs Attribute {dbcs_attr:#x}") return valid - def get_text(self, truncate=True): + def get_text(self, truncate: bool = True) -> str: """A convenience method to extract the text from the _ROW. The _ROW contains a pointer CharRow to an array of CharRowCell objects. Each CharRowCell contains the wide character and an attribute. Enumerating @@ -70,7 +74,6 @@ def get_text(self, truncate=True): for i in range(0, len(char_row), 3) ) except Exception as e: - print(e) line = "" if truncate: @@ -82,62 +85,26 @@ def get_text(self, truncate=True): class ALIAS(objects.StructType): """An Alias Structure""" - def get_source(self): - if self.Source.Length < 8: - return self.Source.Chars.cast( - "string", - encoding="utf-16", - errors="replace", - max_length=self.Source.Length * 2, - ) - elif self.Source.Length < 1024: - return self.Source.Pointer.dereference().cast( - "string", encoding="utf-16", errors="replace", max_length=512 - ) + def get_source(self) -> Union[str, None]: + return self.Source.get_command_string() - return None - - def get_target(self): - if self.Target.Length < 8: - return self.Target.Chars.cast( - "string", - encoding="utf-16", - errors="replace", - max_length=self.Target.Length * 2, - ) - elif self.Target.Length < 1024: - return self.Target.Pointer.dereference().cast( - "string", encoding="utf-16", errors="replace", max_length=512 - ) - - return None + def get_target(self) -> Union[str, None]: + return self.Target.get_command_string() class EXE_ALIAS_LIST(objects.StructType): """An Exe Alias List Structure""" - def get_exename(self): + def get_exename(self) -> Union[str, None]: exe_name = self.ExeName # Windows 10 22000 and Server 20348 removed the Pointer if isinstance(exe_name, objects.Pointer): exe_name = exe_name.dereference() return exe_name.get_string() - if self.ExeName.Length < 8: - return self.ExeName.Chars.cast( - "string", - encoding="utf-16", - errors="replace", - max_length=self.ExeName.Length * 2, - ) - elif self.ExeName.Length < 1024: - return self.ExeName.Pointer.dereference().cast( - "string", encoding="utf-16", errors="replace", max_length=512 - ) - - return None + return exe_name.get_command_string() - def get_aliases(self): + def get_aliases(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Generator for the individual aliases for a particular executable.""" for alias in self.AliasList.to_list( @@ -151,7 +118,7 @@ class SCREEN_INFORMATION(objects.StructType): """A Screen Information Structure.""" @property - def ScreenX(self): + def ScreenX(self) -> int: # 22000 change from an array of pointers to _ROW to an array of _ROW row = self.TextBufferInfo.BufferRows.Rows[0] if hasattr(row, "Row"): @@ -160,10 +127,10 @@ def ScreenX(self): return row.RowLength2 @property - def ScreenY(self): + def ScreenY(self) -> int: return self.TextBufferInfo.BufferCapacity - def _truncate_rows(self, rows): + def _truncate_rows(self, rows: List[str]) -> List[str]: """To truncate empty rows at the end, walk the list backwards and get the last non-empty row. Use that row index to splice. Rows are created based on the @@ -187,7 +154,9 @@ def _truncate_rows(self, rows): return rows - def get_buffer(self, truncate_rows=True, truncate_lines=True): + def get_buffer( + self, truncate_rows: bool = True, truncate_lines: bool = True + ) -> List[str]: """Get the screen buffer. The screen buffer is comprised of the screen's Y @@ -206,7 +175,7 @@ def get_buffer(self, truncate_rows=True, truncate_lines=True): capacity = self.TextBufferInfo.BufferCapacity start = self.TextBufferInfo.BufferStart - buffer_rows = self.TextBufferInfo.BufferRows.dereference() + buffer_rows = self.TextBufferInfo.BufferRows buffer_rows.Rows.count = self.TextBufferInfo.BufferCapacity for i in range(capacity): @@ -234,10 +203,10 @@ class CONSOLE_INFORMATION(objects.StructType): """A Console Information Structure.""" @property - def ScreenBuffer(self): + def ScreenBuffer(self) -> interfaces.objects.ObjectInterface: return self.GetScreenBuffer - def is_valid(self, max_buffers=4) -> bool: + def is_valid(self, max_buffers: int = 4) -> bool: """Determine if the structure is valid.""" # Last displayed must be between -1 and max @@ -249,7 +218,7 @@ def is_valid(self, max_buffers=4) -> bool: return True - def get_screens(self): + def get_screens(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Generator for screens in the console. A console can have multiple screen buffers at a time, @@ -277,14 +246,18 @@ def get_screens(self): seen.add(cur.vol.offset) cur = cur.Next - def get_histories(self): - for cmd_hist in self.HistoryList.dereference().to_list( + def get_histories( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + for cmd_hist in self.HistoryList.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_COMMAND_HISTORY", "ListEntry", ): yield cmd_hist - def get_exe_aliases(self): + def get_exe_aliases( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: exe_alias_list = self.ExeAliasList # Windows 10 22000 and Server 20348 made this a Pointer if isinstance(exe_alias_list, objects.Pointer): @@ -295,14 +268,16 @@ def get_exe_aliases(self): ): yield exe_alias_list_item - def get_processes(self): - for proc in self.ConsoleProcessList.dereference().to_list( + def get_processes( + self, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + for proc in self.ConsoleProcessList.to_list( f"{self.get_symbol_table_name()}{constants.BANG}_CONSOLE_PROCESS_LIST", "ListEntry", ): yield proc - def get_title(self): + def get_title(self) -> Union[str, None]: try: return self.Title.dereference().cast( "string", encoding="utf-16", errors="replace", max_length=512 @@ -310,7 +285,7 @@ def get_title(self): except Exception: return "" - def get_original_title(self): + def get_original_title(self) -> Union[str, None]: try: return self.OriginalTitle.dereference().cast( "string", encoding="utf-16", errors="replace", max_length=512 @@ -322,7 +297,7 @@ def get_original_title(self): class COMMAND(objects.StructType): """A Command Structure""" - def is_valid(self): + def is_valid(self) -> bool: if ( self.Length < 1 or self.Allocated < 1 @@ -333,7 +308,7 @@ def is_valid(self): return True - def get_command(self): + def get_command_string(self) -> Union[str, None]: if self.Length < 8: return self.Chars.cast( "string", @@ -343,7 +318,10 @@ def get_command(self): ) elif self.Length < 1024: return self.Pointer.dereference().cast( - "string", encoding="utf-16", errors="replace", max_length=512 + "string", + encoding="utf-16", + errors="replace", + max_length=self.Length * 2, ) return None @@ -353,17 +331,17 @@ class COMMAND_HISTORY(objects.StructType): """A Command History Structure.""" @property - def CommandCount(self): + def CommandCount(self) -> int: command_type = self.get_symbol_table_name() + constants.BANG + "_COMMAND" command_size = self._context.symbol_space.get_type(command_type).size return int((self.CommandBucket.End - self.CommandBucket.Begin) / command_size) @property - def ProcessHandle(self): + def ProcessHandle(self) -> int: """Allow ProcessHandle to be referenced regardless of OS version""" return self.ConsoleProcessHandle.ProcessHandle - def is_valid(self, max_history=50): + def is_valid(self, max_history: int = 50) -> bool: # The count must be between zero and max if self.CommandCount < 0 or self.CommandCount > max_history: return False @@ -382,22 +360,12 @@ def is_valid(self, max_history=50): return True - def get_application(self): - if self.Application.Length < 8: - return self.Application.Chars.cast( - "string", - encoding="utf-16", - errors="replace", - max_length=self.Application.Length * 2, - ) - elif self.Application.Length < 1024: - return self.Application.Pointer.dereference().cast( - "string", encoding="utf-16", errors="replace", max_length=512 - ) - - return None + def get_application(self) -> Union[str, None]: + return self.Application.get_command_string() - def scan_command_bucket(self, end=None): + def scan_command_bucket( + self, end: Union[int, None] = None + ) -> Generator[Tuple[int, interfaces.objects.ObjectInterface], None, None]: """Brute force print all strings pointed to by the CommandBucket entries by going to greater of EndCapacity or CommandCountMax*sizeof(_COMMAND)""" @@ -418,7 +386,9 @@ def scan_command_bucket(self, end=None): if cmd.is_valid(): yield i, cmd - def get_commands(self): + def get_commands( + self, + ) -> Generator[Tuple[int, interfaces.objects.ObjectInterface], None, None]: """Generator for commands in the history buffer. The CommandBucket is an array of pointers to _COMMAND From 95f56ad122151bf7654dca68f943b7ce5f969f94 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Mon, 7 Oct 2024 12:39:03 -0500 Subject: [PATCH 168/250] #816 - remove extra text from buffer output --- volatility3/framework/plugins/windows/consoles.py | 6 +++++- .../framework/symbols/windows/extensions/consoles.py | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/windows/consoles.py b/volatility3/framework/plugins/windows/consoles.py index cef7cba465..ad1c9d4bde 100644 --- a/volatility3/framework/plugins/windows/consoles.py +++ b/volatility3/framework/plugins/windows/consoles.py @@ -895,7 +895,11 @@ def _generator( if console_property["address"] is None else format_hints.Hex(console_property["address"]) ), - str(console_property["data"]), + ( + str(console_property["data"]) + if console_property["data"] + else renderers.NotAvailableValue() + ), ), ) else: diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 96f7892a52..2312149c71 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -192,10 +192,6 @@ def get_buffer( if truncate_rows: rows = self._truncate_rows(rows) - if rows: - rows = ["=== START OF BUFFER ==="] + rows + ["=== END OF BUFFER ==="] - else: - rows = ["=== NO BUFFER DATA FOUND ==="] return rows From 1c3e5574f4dda317bfde0fdf72805ca488b342a6 Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Mon, 7 Oct 2024 12:39:58 -0500 Subject: [PATCH 169/250] #816 - remove unused import --- volatility3/framework/plugins/windows/cmdscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/cmdscan.py b/volatility3/framework/plugins/windows/cmdscan.py index 9a3460a208..9645ee507f 100644 --- a/volatility3/framework/plugins/windows/cmdscan.py +++ b/volatility3/framework/plugins/windows/cmdscan.py @@ -6,7 +6,7 @@ import logging import struct -from typing import Tuple, Generator, Set, Dict, Any, List, Optional +from typing import Tuple, Generator, Set, Dict, Any, Optional from volatility3.framework import interfaces from volatility3.framework import renderers From 0bfac049defe05ea589dcc6d2b611df5e0eaacd4 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 18 Jul 2024 10:44:51 -0500 Subject: [PATCH 170/250] Windows: Adds Amcache plugin --- .../framework/plugins/windows/amcache.py | 646 ++++++++++++++++++ .../plugins/windows/registry/hivelist.py | 10 +- .../symbols/windows/extensions/registry.py | 62 +- 3 files changed, 700 insertions(+), 18 deletions(-) create mode 100644 volatility3/framework/plugins/windows/amcache.py diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py new file mode 100644 index 0000000000..5838c62a03 --- /dev/null +++ b/volatility3/framework/plugins/windows/amcache.py @@ -0,0 +1,646 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import dataclasses +import datetime +import enum +import logging +import itertools +from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry +from volatility3.framework.renderers import conversion +from volatility3.framework.symbols.windows.extensions import registry as reg_extensions +from volatility3.plugins import timeliner +from volatility3.plugins.windows.registry import hivelist + +vollog = logging.getLogger(__name__) + +####################################################################### +# More information about the following enums can be found in the report +# 'Analysis of the AmCache` by Blanche Lagny, 2019 +####################################################################### + + +class Win8FileValName(enum.Enum): + """ + An enumeration that creates a helpful mapping of opaque Windows 8 Amcache + 'File' subkey value names to their human-readable equivalent. + """ + + ProgramID = "100" + SHA1Hash = "101" + Product = "0" + Company = "1" + Size = "6" + SizeOfImage = "7" + PEHeaderChecksum = "9" + LastModTime = "11" # REG_QWORD FILETIME + CreateTime = "12" # REG_QWORD FILETIME + Path = "15" + LastModTime2 = "17" # REG_QWORD FILETIME + Version = "d" + CompileTime = "f" # REG_QWORD UNIX EPOCH + + +class Win8ProgramValName(enum.Enum): + """ + An enumeration that creates a helpful mapping of opaque Windows 8 Amcache + 'Program' subkey value names to their human-readable equivalent. + """ + + Product = "0" + Version = "1" + Publisher = "2" + InstallTime = "a" + MSIProductCode = "11" + MSIPackageCode = "12" + ProductCode = "f" + PackageCode = "10" + + +class Win10InvAppFileValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryApplicationFile' subkey value names. + """ + + FileId = "FileId" + LinkDate = "LinkDate" + LowerCaseLongPath = "LowerCaseLongPath" + ProductName = "ProductName" + ProductVersion = "ProductVersion" + ProgramID = "ProgramId" + Publisher = "Publisher" + + +class Win10InvAppValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryApplication' subkey value names. + """ + + InstallDate = "InstallDate" + Name = "Name" + Publisher = "Publisher" + RootDirPath = "RootDirPath" + Version = "Version" + + +class Win10DriverBinaryValName(enum.Enum): + """ + An enumeration containing the most useful Windows 10 Amcache + 'InventoryDriverBinary' subkey value names. + """ + + DriverId = "DriverId" + DriverName = "DriverName" + DriverCompany = "DriverCompany" + Product = "Product" + Service = "Service" + DriverTimeStamp = "DriverTimeStamp" + + +class AmcacheEntryType(enum.IntEnum): + Driver = 1 + Program = 2 + File = 3 + + +NullableString = Union[str, None, interfaces.renderers.BaseAbsentValue] +NullableDatetime = Union[datetime.datetime, None, interfaces.renderers.BaseAbsentValue] + + +@dataclasses.dataclass +class _AmcacheEntry: + """ + A class containing all information about an entry from the Amcache registry hive. + Because all values could potentially be paged out of memory or malformed, they are all + a union between their expected value and `interfaces.renderers.BaseAbsentValue`. + """ + + entry_type: str + path: NullableString = renderers.NotApplicableValue() + company: NullableString = renderers.NotApplicableValue() + last_modify_time: NullableDatetime = renderers.NotApplicableValue() + last_modify_time_2: NullableDatetime = renderers.NotApplicableValue() + install_time: NullableDatetime = renderers.NotApplicableValue() + compile_time: NullableDatetime = renderers.NotApplicableValue() + sha1_hash: NullableString = renderers.NotApplicableValue() + service: NullableString = renderers.NotApplicableValue() + product_name: NullableString = renderers.NotApplicableValue() + + +def _entry_sort_key(entry_tuple: Tuple[NullableString, _AmcacheEntry]) -> str: + """Sorts entries by program ID. This is broken out as a function here + to ensure consistency in sorting between the `group_by` and `sorted` function + invocations. + """ + program_id, _ = entry_tuple + key = program_id if isinstance(program_id, str) else "" + return key + + +def _get_string_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableString: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, bytes): + return renderers.UnparsableValue() + + return data.decode("utf-16le", errors="replace").rstrip("\u0000") + + +def _get_datetime_filetime_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, int): + return renderers.UnparsableValue() + + return conversion.wintime_to_datetime(data) + + +def _get_datetime_utc_epoch_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, (int, float)): + return renderers.UnparsableValue() + + try: + return datetime.datetime.fromtimestamp(float(data), datetime.timezone.utc) + except (ValueError, OverflowError, OSError): + return renderers.UnparsableValue() + + +def _get_datetime_str_value( + values: Dict[str, reg_extensions.CM_KEY_VALUE], name: str +) -> NullableDatetime: + try: + value = values[name] + except KeyError: + return renderers.NotAvailableValue() + + data = value.decode_data() + if not isinstance(data, int): + return renderers.UnparsableValue() + + if isinstance(data, str): + try: + return datetime.datetime.strptime(data, "%m/%d/%Y %H:%M:%S") + except ValueError: + return renderers.UnparsableValue() + else: + return renderers.UnparsableValue() + + +class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): + """Scans for windows services.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + ] + + def generate_timeline( + self, + ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: + for _, entry in self._generator(): + if isinstance(entry.last_modify_time, datetime.datetime): + yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time + if isinstance(entry.last_modify_time_2, datetime.datetime): + yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2 + if isinstance(entry.install_time, datetime.datetime): + yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time + if isinstance(entry.compile_time, datetime.datetime): + yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time + + @classmethod + def get_amcache_hive( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel: interfaces.context.ModuleInterface, + ) -> Optional[registry.RegistryHive]: + """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" + return next( + hivelist.HiveList.list_hives( + context=context, + base_config_path=interfaces.configuration.path_join( + config_path, "hivelist" + ), + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="amcache", + ), + None, + ) + + @classmethod + def parse_file_key( + cls, file_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: + """Parses File entries from the Windows 8 `Root\\File` key. + + :param programs_key: The `Root\\File` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. + """ + + for file_entry_key in itertools.chain( + *(key.get_subkeys() for key in file_key.get_subkeys()) + ): + vollog.debug(f"Checking Win8 File key {file_entry_key.get_name()}") + values = { + str(value.get_name()): value + for value in file_entry_key.get_values() + if value.get_name() in [key.value for key in Win8FileValName] + } + + program_id = _get_string_value(values, Win8FileValName.ProgramID.value) + path = _get_string_value(values, Win8FileValName.Path.value) + company = _get_string_value(values, Win8FileValName.Company.value) + last_mod_time = _get_datetime_filetime_value( + values, Win8FileValName.LastModTime.value + ) + last_mod_time_2 = _get_datetime_filetime_value( + values, Win8FileValName.LastModTime2.value + ) + install_time = _get_datetime_filetime_value( + values, Win8FileValName.CreateTime.value + ) + compile_time = _get_datetime_utc_epoch_value( + values, Win8FileValName.CompileTime.value + ) + sha1_hash = _get_string_value(values, Win8FileValName.SHA1Hash.value) + vollog.debug(f"Found sha1hash {sha1_hash}") + product_name = _get_string_value(values, Win8FileValName.Product.value) + + yield program_id, _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=company, + last_modify_time=last_mod_time, + last_modify_time_2=last_mod_time_2, + install_time=install_time, + compile_time=compile_time, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=product_name, + ) + + @classmethod + def parse_programs_key( + cls, programs_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[str, _AmcacheEntry]]: + """Parses Program entries from the Windows 8 `Root\\Programs` key. + + :param programs_key: The `Root\\Programs` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\File` entries, and the second member is the `AmcacheEntry`. + """ + for program_key in programs_key.get_subkeys(): + values = { + str(value.get_name()): value + for value in program_key.get_values() + if value.get_name() in [key.value for key in Win8ProgramValName] + } + vollog.debug(f"Parsing Win8 Program key {program_key.get_name()}") + program_id = program_key.get_name().strip().strip("\u0000") + + product = _get_string_value(values, Win8ProgramValName.Product.value) + company = _get_string_value(values, Win8ProgramValName.Publisher.value) + install_time = _get_datetime_utc_epoch_value( + values, Win8ProgramValName.InstallTime.value + ) + _version = _get_string_value(values, Win8ProgramValName.Version.value) + + if isinstance(_version, str): + if isinstance(product, str): + product = f"{product} {_version}" + else: + product = f"UNKNOWN {_version}" + + yield program_id, _AmcacheEntry( + AmcacheEntryType.Program.name, + company=company, + last_modify_time=conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ), + install_time=install_time, + product_name=product, + ) + + @classmethod + def parse_inventory_app_key( + cls, inv_app_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[str, _AmcacheEntry]]: + """Parses InventoryApplication entries from the Windows 10 `Root\\InventoryApplication` key. + + :param programs_key: The `Root\\InventoryApplication` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\InventoryApplicationFile` entries, and the second member is the `AmcacheEntry`. + """ + for program_key in inv_app_key.get_subkeys(): + program_id = program_key.get_name() + + values = { + str(value.get_name()): value + for value in program_key.get_values() + if value.get_name() in [key.value for key in Win10InvAppValName] + } + + name = _get_string_value(values, Win10InvAppValName.Name.value) + version = _get_string_value(values, Win10InvAppValName.Version.value) + publisher = _get_string_value(values, Win10InvAppValName.Publisher.value) + path = _get_string_value(values, Win10InvAppValName.RootDirPath.value) + install_date = _get_datetime_str_value( + values, Win10InvAppValName.InstallDate.value + ) + last_mod = conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ) + + product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore + if isinstance(version, str): + product += " " + version + + yield program_id.strip().strip("\u0000"), _AmcacheEntry( + AmcacheEntryType.Program.name, + path=path, + last_modify_time=last_mod, + install_time=install_date, + product_name=product, + company=publisher, + ) + + @classmethod + def parse_inventory_app_file_key( + cls, inv_app_file_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[Tuple[NullableString, _AmcacheEntry]]: + """Parses executable file entries from the `Root\\InventoryApplicationFile` registry key. + + :param inv_app_file_key: The `Root\\InventoryApplicationFile` registry key. + :return: An iterator of tuples, where the first member is the program ID string for correlating + with it's parent `InventoryApplication` program entry, and the second member is the `Amcache` entry. + """ + + valName = Win10InvAppFileValName + + for file_key in inv_app_file_key.get_subkeys(): + + vollog.debug( + f"Parsing Win10 InventoryApplicationFile key {file_key.get_name()}" + ) + + values = { + str(value.get_name()): value + for value in file_key.get_values() + if value.get_name() in [key.value for key in valName] + } + + last_mod = conversion.wintime_to_datetime(file_key.LastWriteTime.QuadPart) + path = _get_string_value(values, valName.LowerCaseLongPath.value) + linkdate = _get_datetime_str_value(values, valName.LinkDate.value) + sha1_hash = _get_string_value(values, valName.FileId.value) + publisher = _get_string_value(values, valName.Publisher.value) + prod_name = _get_string_value(values, valName.ProductName.value) + prod_ver = _get_string_value(values, valName.ProductVersion.value) + program_id = _get_string_value(values, valName.ProgramID.value) + + if isinstance(prod_ver, str): + if isinstance(prod_name, str): + prod_name = f"{prod_name} {prod_ver}" + else: + prod_name = f"UNKNOWN {prod_ver}" + + yield program_id, _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=publisher, + last_modify_time=last_mod, + compile_time=linkdate, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=prod_name, + ) + + @classmethod + def parse_driver_binary_key( + cls, driver_binary_key: reg_extensions.CM_KEY_NODE + ) -> Iterator[_AmcacheEntry]: + """Parses information about installed drivers from the `Root\\InventoryDriverBinary` registry key. + + :param driver_binary_key: The `Root\\InventoryDriverBinary` registry key + :return: An iterator of `AmcacheEntry`s + """ + for binary_key in driver_binary_key.get_subkeys(): + + valName = Win10DriverBinaryValName + + values = { + str(value.get_name()): value + for value in binary_key.get_values() + if value.get_name() in [key.value for key in valName] + } + + # Depending on the Windows version, the key name will be either the name + # of the driver, or its SHA1 hash. + if "/" in binary_key.get_name(): + driver_name = binary_key.get_name() + sha1_hash = _get_string_value(values, valName.DriverId.name) + else: + sha1_hash = binary_key.get_name() + driver_name = _get_string_value(values, valName.DriverName.name) + + if isinstance(sha1_hash, str): + sha1_hash = sha1_hash[4:] if sha1_hash.startswith("0000") else sha1_hash + + company, product, service, last_write_time, driver_timestamp = ( + _get_string_value(values, valName.DriverCompany.name), + _get_string_value(values, valName.Product.name), + _get_string_value(values, valName.Service.name), + conversion.wintime_to_datetime(binary_key.LastWriteTime.QuadPart), + _get_datetime_utc_epoch_value(values, valName.DriverTimeStamp.name), + ) + + yield _AmcacheEntry( + entry_type=AmcacheEntryType.Driver.name, + path=driver_name, + company=company, + last_modify_time=last_write_time, + compile_time=driver_timestamp, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + service=service, + product_name=product, + ) + + def _generator(self) -> Iterator[Tuple[int, _AmcacheEntry]]: + kernel = self.context.modules[self.config["kernel"]] + + def indented( + entry_gen: Iterable[_AmcacheEntry], indent: int = 0 + ) -> Iterator[Tuple[int, _AmcacheEntry]]: + for item in entry_gen: + yield indent, item + + # Building the dictionary ahead of time is much better for performance + # vs looking up each service's DLL individually. + amcache = self.get_amcache_hive(self.context, self.config_path, kernel) + if amcache is None: + return + + try: + yield from indented( + self.parse_driver_binary_key( + amcache.get_key("Root\\InventoryDriverBinary") # type: ignore + ) + ) + except KeyError: + # Registry key not found + pass + + try: + programs: Dict[str, _AmcacheEntry] = { + program_id: entry + for program_id, entry in self.parse_programs_key( + amcache.get_key("Root\\Programs") + ) # type: ignore + } + except KeyError: + programs = {} + + try: + files = sorted( + list( + self.parse_file_key(amcache.get_key("Root\\File")), # type: ignore + ), + key=_entry_sort_key, + ) + except KeyError: + files = [] + + for program_id, file_entries in itertools.groupby( + files, + key=_entry_sort_key, + ): + files_indent = 0 + if isinstance(program_id, str): + try: + program_entry = programs.pop(program_id.strip().strip("\u0000")) + yield (0, program_entry) + + files_indent = 1 + except KeyError: + # No parent program for this file entry + pass + for _, entry in file_entries: + yield files_indent, entry + + for empty_program in programs.values(): + yield 0, empty_program + + try: + programs: Dict[str, _AmcacheEntry] = dict( + self.parse_inventory_app_key( + amcache.get_key("Root\\InventoryApplication") # type: ignore + ) + ) + except KeyError: + programs = {} + + try: + files = sorted( + list( + self.parse_inventory_app_file_key(amcache.get_key("Root\\InventoryApplicationFile")), # type: ignore + ), + key=_entry_sort_key, + ) + except KeyError: + files = [] + + for program_id, file_entries in itertools.groupby( + files, + key=_entry_sort_key, + ): + files_indent = 0 + + if isinstance(program_id, str): + try: + program_entry = programs.pop(program_id.strip().strip("\u0000")) + yield (0, program_entry) + files_indent = 1 + except KeyError: + # No parent program for this file entry + pass + + for _, entry in file_entries: + yield files_indent, entry + + for empty_program in programs.values(): + yield 0, empty_program + + def run(self): + + return renderers.TreeGrid( + [ + ("EntryType", str), + ("Path", str), + ("Company", str), + ("LastModifyTime", datetime.datetime), + ("LastModifyTime2", datetime.datetime), + ("InstallTime", datetime.datetime), + ("CompileTime", datetime.datetime), + ("SHA1", str), + ("Service", str), + ("ProductName", str), + ], + ( + (indent, dataclasses.astuple(entry)) + for indent, entry in self._generator() + ), + ) diff --git a/volatility3/framework/plugins/windows/registry/hivelist.py b/volatility3/framework/plugins/windows/registry/hivelist.py index 1cc76dad68..ddc9c18555 100644 --- a/volatility3/framework/plugins/windows/registry/hivelist.py +++ b/volatility3/framework/plugins/windows/registry/hivelist.py @@ -2,9 +2,9 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import Iterator, List, Tuple, Iterable, Optional +from typing import Iterator, List, Optional, Tuple -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers import registry from volatility3.framework.renderers import format_hints @@ -140,8 +140,8 @@ def list_hives( layer_name: str, symbol_table: str, filter_string: Optional[str] = None, - hive_offsets: List[int] = None, - ) -> Iterable[registry.RegistryHive]: + hive_offsets: Optional[List[int]] = None, + ) -> Iterator[registry.RegistryHive]: """Walks through a registry, hive by hive returning the constructed registry layer name. @@ -200,7 +200,7 @@ def list_hive_objects( context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - filter_string: str = None, + filter_string: Optional[str] = None, ) -> Iterator[interfaces.objects.ObjectInterface]: """Lists all the hives in the primary layer. diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index 51be0841cf..2e401caa36 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -5,7 +5,7 @@ import enum import logging import struct -from typing import Iterable, Optional, Union +from typing import Iterator, Optional, Union, cast from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.layers.registry import ( @@ -39,6 +39,29 @@ def _missing_(cls, value): return cls(RegValueTypes.REG_UNKNOWN) +INTEGER_TYPES = [ + RegValueTypes.REG_DWORD, + RegValueTypes.REG_QWORD, + RegValueTypes.REG_DWORD_BIG_ENDIAN, + RegValueTypes.REG_DWORD_BIG_ENDIAN, +] + +STRING_TYPES = [ + RegValueTypes.REG_SZ, + RegValueTypes.REG_MULTI_SZ, + RegValueTypes.REG_EXPAND_SZ, + RegValueTypes.REG_LINK, +] + +BINARY_TYPES = [ + RegValueTypes.REG_RESOURCE_LIST, + RegValueTypes.REG_BINARY, + RegValueTypes.REG_FULL_RESOURCE_DESCRIPTOR, + RegValueTypes.REG_RESOURCE_REQUIREMENTS_LIST, + RegValueTypes.REG_NONE, +] + + class RegKeyFlags(enum.IntEnum): KEY_IS_VOLATILE = 0x01 KEY_HIVE_EXIT = 0x02 @@ -142,7 +165,7 @@ def get_volatile(self) -> bool: ) return bool(self.vol.offset & 0x80000000) - def get_subkeys(self) -> Iterable[interfaces.objects.ObjectInterface]: + def get_subkeys(self) -> Iterator["CM_KEY_NODE"]: """Returns a list of the key nodes.""" hive = self._context.layers[self.vol.layer_name] if not isinstance(hive, RegistryHive): @@ -152,9 +175,18 @@ def get_subkeys(self) -> Iterable[interfaces.objects.ObjectInterface]: subkey_node = hive.get_cell(self.SubKeyLists[index]).u.KeyIndex yield from self._get_subkeys_recursive(hive, subkey_node) + def get_value(self, value_name) -> Optional["CM_KEY_VALUE"]: + for value in self.get_values(): + if value.name == value_name: + return value + return next( + (value for value in self.get_values() if value.get_name() == value_name), + None, + ) + def _get_subkeys_recursive( self, hive: RegistryHive, node: interfaces.objects.ObjectInterface - ) -> Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterator["CM_KEY_NODE"]: """Recursively descend a node returning subkeys.""" # The keylist appears to include 4 bytes of key name after each value # We can either double the list and only use the even items, or @@ -170,7 +202,7 @@ def _get_subkeys_recursive( elif signature == "lh" or signature == "lf": listjump = 2 elif node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"): - yield node + yield cast("CM_KEY_NODE", node) else: vollog.debug( "Unexpected node type encountered when traversing subkeys: {}, signature: {}".format( @@ -200,7 +232,7 @@ def _get_subkeys_recursive( continue yield from self._get_subkeys_recursive(hive, subnode) - def get_values(self) -> Iterable[interfaces.objects.ObjectInterface]: + def get_values(self) -> Iterator["CM_KEY_VALUE"]: """Returns a list of the Value nodes for a key.""" hive = self._context.layers[self.vol.layer_name] if not isinstance(hive, RegistryHive): @@ -216,13 +248,14 @@ def get_values(self) -> Iterable[interfaces.objects.ObjectInterface]: except (RegistryInvalidIndex, RegistryFormatException) as excp: vollog.debug(f"Invalid address {excp}") continue - if node.vol.type_name.endswith(constants.BANG + "_CM_KEY_VALUE"): + if isinstance(node, CM_KEY_VALUE): yield node + except (exceptions.InvalidAddressException, RegistryFormatException) as excp: vollog.debug(f"Invalid address in get_values iteration: {excp}") return None - def get_name(self) -> interfaces.objects.ObjectInterface: + def get_name(self) -> str: """Gets the name for the current key node""" namelength = self.NameLength self.Name.count = namelength @@ -249,6 +282,10 @@ def get_name(self) -> interfaces.objects.ObjectInterface: self.Name.count = namelength return self.Name.cast("string", max_length=namelength, encoding="latin-1") + def get_type(self) -> RegValueTypes: + """Get the type of the registry value""" + return RegValueTypes(self.Type) + def decode_data(self) -> Union[int, bytes]: """Properly decodes the data associated with the value node""" # Determine if the data is stored inline @@ -293,29 +330,28 @@ def decode_data(self) -> Union[int, bytes]: # but the length at the start could be negative so just adding 4 to jump past it data = layer.read(self.Data + 4, datalen) - self_type = RegValueTypes(self.Type) - if self_type == RegValueTypes.REG_DWORD: + if self.get_type() == RegValueTypes.REG_DWORD: if len(data) != struct.calcsize("L"): raise ValueError( f"Size of data does not match the type of registry value {self.get_name()}" ) (res,) = struct.unpack(">L", data) return res - if self_type == RegValueTypes.REG_QWORD: + if self.get_type() == RegValueTypes.REG_QWORD: if len(data) != struct.calcsize(" Union[int, bytes]: RegValueTypes.REG_RESOURCE_REQUIREMENTS_LIST, ]: return data - if self_type == RegValueTypes.REG_NONE: + if self.get_type() == RegValueTypes.REG_NONE: return b"" # Fall back if it's something weird From f3f5784c717acdd1a6b988cb006257ea828be87e Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Oct 2024 13:14:32 -0500 Subject: [PATCH 171/250] Remove unused method --- .../framework/symbols/windows/extensions/registry.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index 2e401caa36..9b09c7d98a 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -175,15 +175,6 @@ def get_subkeys(self) -> Iterator["CM_KEY_NODE"]: subkey_node = hive.get_cell(self.SubKeyLists[index]).u.KeyIndex yield from self._get_subkeys_recursive(hive, subkey_node) - def get_value(self, value_name) -> Optional["CM_KEY_VALUE"]: - for value in self.get_values(): - if value.name == value_name: - return value - return next( - (value for value in self.get_values() if value.get_name() == value_name), - None, - ) - def _get_subkeys_recursive( self, hive: RegistryHive, node: interfaces.objects.ObjectInterface ) -> Iterator["CM_KEY_NODE"]: From e46d24397db29f3eb61a872b3df62b4974f24041 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Oct 2024 13:17:01 -0500 Subject: [PATCH 172/250] Code review fixes --- .../framework/plugins/windows/amcache.py | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index 5838c62a03..33bae718d4 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -5,8 +5,8 @@ import dataclasses import datetime import enum -import logging import itertools +import logging from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union from volatility3.framework import interfaces, renderers @@ -132,6 +132,7 @@ class _AmcacheEntry: sha1_hash: NullableString = renderers.NotApplicableValue() service: NullableString = renderers.NotApplicableValue() product_name: NullableString = renderers.NotApplicableValue() + product_version: NullableString = renderers.NotApplicableValue() def _entry_sort_key(entry_tuple: Tuple[NullableString, _AmcacheEntry]) -> str: @@ -279,6 +280,10 @@ def parse_file_key( correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. """ + val_enum = Win8FileValName + + wanted_values = [key.value for key in val_enum] + for file_entry_key in itertools.chain( *(key.get_subkeys() for key in file_key.get_subkeys()) ): @@ -286,27 +291,27 @@ def parse_file_key( values = { str(value.get_name()): value for value in file_entry_key.get_values() - if value.get_name() in [key.value for key in Win8FileValName] + if value.get_name() in wanted_values } - program_id = _get_string_value(values, Win8FileValName.ProgramID.value) - path = _get_string_value(values, Win8FileValName.Path.value) - company = _get_string_value(values, Win8FileValName.Company.value) + program_id = _get_string_value(values, val_enum.ProgramID.value) + path = _get_string_value(values, val_enum.Path.value) + company = _get_string_value(values, val_enum.Company.value) last_mod_time = _get_datetime_filetime_value( - values, Win8FileValName.LastModTime.value + values, val_enum.LastModTime.value ) last_mod_time_2 = _get_datetime_filetime_value( - values, Win8FileValName.LastModTime2.value + values, val_enum.LastModTime2.value ) install_time = _get_datetime_filetime_value( - values, Win8FileValName.CreateTime.value + values, val_enum.CreateTime.value ) compile_time = _get_datetime_utc_epoch_value( - values, Win8FileValName.CompileTime.value + values, val_enum.CompileTime.value ) - sha1_hash = _get_string_value(values, Win8FileValName.SHA1Hash.value) + sha1_hash = _get_string_value(values, val_enum.SHA1Hash.value) vollog.debug(f"Found sha1hash {sha1_hash}") - product_name = _get_string_value(values, Win8FileValName.Product.value) + product_name = _get_string_value(values, val_enum.Product.value) yield program_id, _AmcacheEntry( AmcacheEntryType.File.name, @@ -335,21 +340,24 @@ def parse_programs_key( :return: An iterator of tuples, where the first member is the program ID string for correlating `Root\\File` entries, and the second member is the `AmcacheEntry`. """ + val_enum = Win8ProgramValName + + wanted_values = [key.value for key in val_enum] for program_key in programs_key.get_subkeys(): values = { str(value.get_name()): value for value in program_key.get_values() - if value.get_name() in [key.value for key in Win8ProgramValName] + if value.get_name() in wanted_values } vollog.debug(f"Parsing Win8 Program key {program_key.get_name()}") program_id = program_key.get_name().strip().strip("\u0000") - product = _get_string_value(values, Win8ProgramValName.Product.value) - company = _get_string_value(values, Win8ProgramValName.Publisher.value) + product = _get_string_value(values, val_enum.Product.value) + company = _get_string_value(values, val_enum.Publisher.value) install_time = _get_datetime_utc_epoch_value( - values, Win8ProgramValName.InstallTime.value + values, val_enum.InstallTime.value ) - _version = _get_string_value(values, Win8ProgramValName.Version.value) + _version = _get_string_value(values, val_enum.Version.value) if isinstance(_version, str): if isinstance(product, str): @@ -378,29 +386,29 @@ def parse_inventory_app_key( :return: An iterator of tuples, where the first member is the program ID string for correlating `Root\\InventoryApplicationFile` entries, and the second member is the `AmcacheEntry`. """ + val_enum = Win10InvAppValName + + wanted_values = [key.value for key in val_enum] + for program_key in inv_app_key.get_subkeys(): program_id = program_key.get_name() values = { str(value.get_name()): value for value in program_key.get_values() - if value.get_name() in [key.value for key in Win10InvAppValName] + if value.get_name() in wanted_values } - name = _get_string_value(values, Win10InvAppValName.Name.value) - version = _get_string_value(values, Win10InvAppValName.Version.value) - publisher = _get_string_value(values, Win10InvAppValName.Publisher.value) - path = _get_string_value(values, Win10InvAppValName.RootDirPath.value) - install_date = _get_datetime_str_value( - values, Win10InvAppValName.InstallDate.value - ) + name = _get_string_value(values, val_enum.Name.value) + version = _get_string_value(values, val_enum.Version.value) + publisher = _get_string_value(values, val_enum.Publisher.value) + path = _get_string_value(values, val_enum.RootDirPath.value) + install_date = _get_datetime_str_value(values, val_enum.InstallDate.value) last_mod = conversion.wintime_to_datetime( program_key.LastWriteTime.QuadPart ) product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore - if isinstance(version, str): - product += " " + version yield program_id.strip().strip("\u0000"), _AmcacheEntry( AmcacheEntryType.Program.name, @@ -409,6 +417,7 @@ def parse_inventory_app_key( install_time=install_date, product_name=product, company=publisher, + product_version=version, ) @classmethod @@ -422,7 +431,9 @@ def parse_inventory_app_file_key( with it's parent `InventoryApplication` program entry, and the second member is the `Amcache` entry. """ - valName = Win10InvAppFileValName + val_enum = Win10InvAppFileValName + + wanted_values = [key.value for key in val_enum] for file_key in inv_app_file_key.get_subkeys(): @@ -433,23 +444,17 @@ def parse_inventory_app_file_key( values = { str(value.get_name()): value for value in file_key.get_values() - if value.get_name() in [key.value for key in valName] + if value.get_name() in wanted_values } last_mod = conversion.wintime_to_datetime(file_key.LastWriteTime.QuadPart) - path = _get_string_value(values, valName.LowerCaseLongPath.value) - linkdate = _get_datetime_str_value(values, valName.LinkDate.value) - sha1_hash = _get_string_value(values, valName.FileId.value) - publisher = _get_string_value(values, valName.Publisher.value) - prod_name = _get_string_value(values, valName.ProductName.value) - prod_ver = _get_string_value(values, valName.ProductVersion.value) - program_id = _get_string_value(values, valName.ProgramID.value) - - if isinstance(prod_ver, str): - if isinstance(prod_name, str): - prod_name = f"{prod_name} {prod_ver}" - else: - prod_name = f"UNKNOWN {prod_ver}" + path = _get_string_value(values, val_enum.LowerCaseLongPath.value) + linkdate = _get_datetime_str_value(values, val_enum.LinkDate.value) + sha1_hash = _get_string_value(values, val_enum.FileId.value) + publisher = _get_string_value(values, val_enum.Publisher.value) + prod_name = _get_string_value(values, val_enum.ProductName.value) + prod_ver = _get_string_value(values, val_enum.ProductVersion.value) + program_id = _get_string_value(values, val_enum.ProgramID.value) yield program_id, _AmcacheEntry( AmcacheEntryType.File.name, @@ -463,6 +468,7 @@ def parse_inventory_app_file_key( else sha1_hash ), product_name=prod_name, + product_version=prod_ver, ) @classmethod @@ -474,34 +480,36 @@ def parse_driver_binary_key( :param driver_binary_key: The `Root\\InventoryDriverBinary` registry key :return: An iterator of `AmcacheEntry`s """ - for binary_key in driver_binary_key.get_subkeys(): + val_enum = Win10DriverBinaryValName + + wanted_values = [key.value for key in val_enum] - valName = Win10DriverBinaryValName + for binary_key in driver_binary_key.get_subkeys(): values = { str(value.get_name()): value for value in binary_key.get_values() - if value.get_name() in [key.value for key in valName] + if value.get_name() in wanted_values } # Depending on the Windows version, the key name will be either the name # of the driver, or its SHA1 hash. if "/" in binary_key.get_name(): driver_name = binary_key.get_name() - sha1_hash = _get_string_value(values, valName.DriverId.name) + sha1_hash = _get_string_value(values, val_enum.DriverId.name) else: sha1_hash = binary_key.get_name() - driver_name = _get_string_value(values, valName.DriverName.name) + driver_name = _get_string_value(values, val_enum.DriverName.name) if isinstance(sha1_hash, str): sha1_hash = sha1_hash[4:] if sha1_hash.startswith("0000") else sha1_hash company, product, service, last_write_time, driver_timestamp = ( - _get_string_value(values, valName.DriverCompany.name), - _get_string_value(values, valName.Product.name), - _get_string_value(values, valName.Service.name), + _get_string_value(values, val_enum.DriverCompany.name), + _get_string_value(values, val_enum.Product.name), + _get_string_value(values, val_enum.Service.name), conversion.wintime_to_datetime(binary_key.LastWriteTime.QuadPart), - _get_datetime_utc_epoch_value(values, valName.DriverTimeStamp.name), + _get_datetime_utc_epoch_value(values, val_enum.DriverTimeStamp.name), ) yield _AmcacheEntry( From e622acc7f8bd458b895d89a7a8922c1ee3f92e0e Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Oct 2024 13:17:44 -0500 Subject: [PATCH 173/250] Minor version bump: registry extensions API addition --- volatility3/framework/constants/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index a131609c93..ce803a6877 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,6 +1,6 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 10 # Number of changes that only add to the interface +VERSION_MINOR = 11 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" From dec0d506f91c87f5ceced7a12766f746f0df68e3 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Oct 2024 13:54:07 -0500 Subject: [PATCH 174/250] Fix type signature on get_name --- volatility3/framework/symbols/windows/extensions/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index 9b09c7d98a..bebfaea891 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -246,7 +246,7 @@ def get_values(self) -> Iterator["CM_KEY_VALUE"]: vollog.debug(f"Invalid address in get_values iteration: {excp}") return None - def get_name(self) -> str: + def get_name(self) -> interfaces.objects.ObjectInterface: """Gets the name for the current key node""" namelength = self.NameLength self.Name.count = namelength From 27ea555b378b89b005bb030ecbdd6f94c872d966 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 7 Oct 2024 13:54:30 -0500 Subject: [PATCH 175/250] Add product version to output columns --- volatility3/framework/plugins/windows/amcache.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index 33bae718d4..8d0a4769ea 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -357,13 +357,7 @@ def parse_programs_key( install_time = _get_datetime_utc_epoch_value( values, val_enum.InstallTime.value ) - _version = _get_string_value(values, val_enum.Version.value) - - if isinstance(_version, str): - if isinstance(product, str): - product = f"{product} {_version}" - else: - product = f"UNKNOWN {_version}" + version = _get_string_value(values, val_enum.Version.value) yield program_id, _AmcacheEntry( AmcacheEntryType.Program.name, @@ -373,6 +367,7 @@ def parse_programs_key( ), install_time=install_time, product_name=product, + product_version=version, ) @classmethod @@ -494,11 +489,11 @@ def parse_driver_binary_key( # Depending on the Windows version, the key name will be either the name # of the driver, or its SHA1 hash. - if "/" in binary_key.get_name(): - driver_name = binary_key.get_name() + if "/" in str(binary_key.get_name()): + driver_name = str(binary_key.get_name()) sha1_hash = _get_string_value(values, val_enum.DriverId.name) else: - sha1_hash = binary_key.get_name() + sha1_hash = str(binary_key.get_name()) driver_name = _get_string_value(values, val_enum.DriverName.name) if isinstance(sha1_hash, str): @@ -646,6 +641,7 @@ def run(self): ("SHA1", str), ("Service", str), ("ProductName", str), + ("ProductVersion", str), ], ( (indent, dataclasses.astuple(entry)) From 996875c059bd8f817e98a40bd7e782070798275d Mon Sep 17 00:00:00 2001 From: Dave Lassalle Date: Mon, 7 Oct 2024 16:13:12 -0500 Subject: [PATCH 176/250] #1298 - include hive name in key path --- volatility3/framework/plugins/windows/registry/printkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/registry/printkey.py b/volatility3/framework/plugins/windows/registry/printkey.py index 180f8f9d9f..4fe3f97fb8 100644 --- a/volatility3/framework/plugins/windows/registry/printkey.py +++ b/volatility3/framework/plugins/windows/registry/printkey.py @@ -20,7 +20,7 @@ class PrintKey(interfaces.plugins.PluginInterface): """Lists the registry keys under a hive or specific key value.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -282,7 +282,7 @@ def _registry_walker( renderers.UnreadableValue(), format_hints.Hex(hive.hive_offset), "Key", - "?\\" + (key or ""), + f"{hive.get_name()}\\" + (key or ""), renderers.UnreadableValue(), renderers.UnreadableValue(), renderers.UnreadableValue(), From 9addf6bee815bf6ad4a3333bb2ba155a5ad8ed06 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 7 Oct 2024 22:56:20 +0100 Subject: [PATCH 177/250] Remove the old .style.yapf This was used before we shifted to black permanently. It allowed additional spacing between keyword arguments and their default values, but it was inconsisnent and could get in a state where it would flip between two weights, so every "fix" would then be "fixed" back again. No longer feasible for a project of this size. --- .style.yapf | 261 ---------------------------------------------------- 1 file changed, 261 deletions(-) delete mode 100644 .style.yapf diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index 3f154e07b7..0000000000 --- a/.style.yapf +++ /dev/null @@ -1,261 +0,0 @@ -[style] -# Align closing bracket with visual indentation. -align_closing_bracket_with_visual_indent=True - -# Allow dictionary keys to exist on multiple lines. For example: -# -# x = { -# ('this is the first element of a tuple', -# 'this is the second element of a tuple'): -# value, -# } -allow_multiline_dictionary_keys=False - -# Allow lambdas to be formatted on more than one line. -allow_multiline_lambdas=False - -# Allow splits before the dictionary value. -allow_split_before_dict_value=True - -# Number of blank lines surrounding top-level function and class -# definitions. -blank_lines_around_top_level_definition=2 - -# Insert a blank line before a class-level docstring. -blank_line_before_class_docstring=False - -# Insert a blank line before a module docstring. -blank_line_before_module_docstring=False - -# Insert a blank line before a 'def' or 'class' immediately nested -# within another 'def' or 'class'. For example: -# -# class Foo: -# # <------ this blank line -# def method(): -# ... -blank_line_before_nested_class_or_def=True - -# Do not split consecutive brackets. Only relevant when -# dedent_closing_brackets is set. For example: -# -# call_func_that_takes_a_dict( -# { -# 'key1': 'value1', -# 'key2': 'value2', -# } -# ) -# -# would reformat to: -# -# call_func_that_takes_a_dict({ -# 'key1': 'value1', -# 'key2': 'value2', -# }) -coalesce_brackets=False - -# The column limit. -column_limit=120 - -# The style for continuation alignment. Possible values are: -# -# - SPACE: Use spaces for continuation alignment. This is default behavior. -# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns -# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation -# alignment. -# - LESS: Slightly left if cannot vertically align continuation lines with -# indent characters. -# - VALIGN-RIGHT: Vertically align continuation lines with indent -# characters. Slightly right (one more indent character) if cannot -# vertically align continuation lines with indent characters. -# -# For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is -# enabled. -continuation_align_style=SPACE - -# Indent width used for line continuations. -continuation_indent_width=4 - -# Put closing brackets on a separate line, dedented, if the bracketed -# expression can't fit in a single line. Applies to all kinds of brackets, -# including function definitions and calls. For example: -# -# config = { -# 'key1': 'value1', -# 'key2': 'value2', -# } # <--- this bracket is dedented and on a separate line -# -# time_series = self.remote_client.query_entity_counters( -# entity='dev3246.region1', -# key='dns.query_latency_tcp', -# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), -# start_ts=now()-timedelta(days=3), -# end_ts=now(), -# ) # <--- this bracket is dedented and on a separate line -dedent_closing_brackets=False - -# Disable the heuristic which places each list element on a separate line -# if the list is comma-terminated. -disable_ending_comma_heuristic=False - -# Place each dictionary entry onto its own line. -each_dict_entry_on_separate_line=True - -# The regex for an i18n comment. The presence of this comment stops -# reformatting of that line, because the comments are required to be -# next to the string they translate. -i18n_comment= - -# The i18n function call names. The presence of this function stops -# reformatting on that line, because the string it has cannot be moved -# away from the i18n comment. -i18n_function_call= - -# Indent the dictionary value if it cannot fit on the same line as the -# dictionary key. For example: -# -# config = { -# 'key1': -# 'value1', -# 'key2': value1 + -# value2, -# } -indent_dictionary_value=False - -# The number of columns to use for indentation. -indent_width=4 - -# Join short lines into one line. E.g., single line 'if' statements. -join_multiple_lines=True - -# Do not include spaces around selected binary operators. For example: -# -# 1 + 2 * 3 - 4 / 5 -# -# will be formatted as follows when configured with "*,/": -# -# 1 + 2*3 - 4/5 -# -no_spaces_around_selected_binary_operators= - -# Use spaces around default or named assigns. -spaces_around_default_or_named_assign=True - -# Use spaces around the power operator. -spaces_around_power_operator=True - -# The number of spaces required before a trailing comment. -spaces_before_comment=2 - -# Insert a space between the ending comma and closing bracket of a list, -# etc. -space_between_ending_comma_and_closing_bracket=True - -# Split before arguments -split_all_comma_separated_values=False - -# Split before arguments if the argument list is terminated by a -# comma. -split_arguments_when_comma_terminated=False - -# Set to True to prefer splitting before '&', '|' or '^' rather than -# after. -split_before_bitwise_operator=True - -# Split before the closing bracket if a list or dict literal doesn't fit on -# a single line. -split_before_closing_bracket=True - -# Split before a dictionary or set generator (comp_for). For example, note -# the split before the 'for': -# -# foo = { -# variable: 'Hello world, have a nice day!' -# for variable in bar if variable != 42 -# } -split_before_dict_set_generator=True - -# Split before the '.' if we need to split a longer expression: -# -# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d)) -# -# would reformat to something like: -# -# foo = ('This is a really long string: {}, {}, {}, {}' -# .format(a, b, c, d)) -split_before_dot=False - -# Split after the opening paren which surrounds an expression if it doesn't -# fit on a single line. -split_before_expression_after_opening_paren=False - -# If an argument / parameter list is going to be split, then split before -# the first argument. -split_before_first_argument=False - -# Set to True to prefer splitting before 'and' or 'or' rather than -# after. -split_before_logical_operator=True - -# Split named assignments onto individual lines. -split_before_named_assigns=True - -# Set to True to split list comprehensions and generators that have -# non-trivial expressions and multiple clauses before each of these -# clauses. For example: -# -# result = [ -# a_long_var + 100 for a_long_var in xrange(1000) -# if a_long_var % 10] -# -# would reformat to something like: -# -# result = [ -# a_long_var + 100 -# for a_long_var in xrange(1000) -# if a_long_var % 10] -split_complex_comprehension=True - -# The penalty for splitting right after the opening bracket. -split_penalty_after_opening_bracket=200 - -# The penalty for splitting the line after a unary operator. -split_penalty_after_unary_operator=10000 - -# The penalty for splitting right before an if expression. -split_penalty_before_if_expr=0 - -# The penalty of splitting the line around the '&', '|', and '^' -# operators. -split_penalty_bitwise_operator=300 - -# The penalty for splitting a list comprehension or generator -# expression. -split_penalty_comprehension=80 - -# The penalty for characters over the column limit. -split_penalty_excess_character=7000 - -# The penalty incurred by adding a line split to the unwrapped line. The -# more line splits added the higher the penalty. -split_penalty_for_added_line_split=30 - -# The penalty of splitting a list of "import as" names. For example: -# -# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, -# long_argument_2, -# long_argument_3) -# -# would reformat to something like: -# -# from a_very_long_or_indented_module_name_yada_yad import ( -# long_argument_1, long_argument_2, long_argument_3) -split_penalty_import_names=0 - -# The penalty of splitting the line around the 'and' and 'or' -# operators. -split_penalty_logical_operator=300 - -# Use the Tab character for indentation. -use_tabs=False - From e419b67581e6d31d7959480d5904e283fa2929a1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 8 Oct 2024 02:06:04 +0200 Subject: [PATCH 178/250] introduce dirty state ioc --- .../framework/plugins/windows/malfind.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 079274d698..5ce2279ccb 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -122,10 +122,30 @@ def list_injections( vadinfo.winnt_protections, ) write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string + dirty_page_check = False - # the write/exec check applies to everything if not write_exec: - continue + """ + # Inspect "PAGE_EXECUTE_READ" VAD pages to detect + # non writable memory regions having been injected + # using elevated WriteProcessMemory(). + """ + if "EXECUTE" in protection_string: + for page in range( + vad.get_start(), vad.get_end(), proc_layer.page_size + ): + try: + # If we have a dirty page in a non writable "EXECUTE" region, it is suspicious. + if proc_layer.is_dirty(page): + dirty_page_check = True + break + except exceptions.InvalidAddressException: + # Abort as it is likely that other addresses in the same range will also fail + break + if not dirty_page_check: + continue + else: + continue if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( vad.get_private_memory() == 0 @@ -134,6 +154,11 @@ def list_injections( if cls.is_vad_empty(proc_layer, vad): continue + if dirty_page_check: + # Useful information to investigate the page content with volshell afterwards. + vollog.warning( + f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(page)}", + ) data = proc_layer.read(vad.get_start(), 64, pad=True) yield vad, data From f92dfdd33b504e781d9ba660b8fba625618e33a9 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 15:53:07 +1100 Subject: [PATCH 179/250] Linux: ptrace: remove patch number in plugin requirement --- volatility3/framework/plugins/linux/ptrace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index 793a1b3a1e..1a37b8782e 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -31,7 +31,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] architectures=LINUX_ARCHS, ), requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(2, 2, 1) + name="pslist", plugin=pslist.PsList, version=(2, 2, 0) ), ] From eb2d4b176518d8337a82ebc61b99ed04186904e3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 15:54:48 +1100 Subject: [PATCH 180/250] Linux: ptrace: add ptrace functions to the task_struct object extension --- .../symbols/linux/extensions/__init__.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fdd34403ac..7f6d443197 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,13 +13,12 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion -from volatility3.framework.configuration import requirements from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES +from volatility3.framework.constants.linux import CAPABILITIES, PT_FLAGS from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed @@ -374,6 +373,41 @@ def get_threads(self) -> Iterable[interfaces.objects.ObjectInterface]: ): yield task + @property + def is_being_ptraced(self) -> bool: + """Returns True if this task is being traced using ptrace""" + return self.ptrace != 0 + + @property + def is_ptracing(self) -> bool: + """Returns True if this task is tracing other tasks using ptrace""" + is_tracing = ( + self.ptraced.next.is_readable() + and self.ptraced.next.dereference().vol.offset != self.ptraced.vol.offset + ) + return is_tracing + + def get_ptrace_tracer_tid(self) -> Optional[int]: + """Returns the tracer's TID tracing this task""" + return self.parent.pid if self.is_being_ptraced else None + + def get_ptrace_tracee_tids(self) -> List[int]: + """Returns the list of TIDs being traced by this task""" + task_symbol_table_name = self.get_symbol_table_name() + + task_struct_symname = f"{task_symbol_table_name}{constants.BANG}task_struct" + tracing_tid_list = [ + task_being_traced.pid + for task_being_traced in self.ptraced.to_list( + task_struct_symname, "ptrace_entry" + ) + ] + return tracing_tid_list + + def get_ptrace_tracee_flags(self) -> Optional[str]: + """Returns a string with the ptrace flags""" + return PT_FLAGS(self.ptrace).flags if self.is_being_ptraced else None + class fs_struct(objects.StructType): def get_root_dentry(self): From 3c4b9996ce78a00ef5121782fd75a45f90a557ae Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 16:00:35 +1100 Subject: [PATCH 181/250] Linux: ptrace: move tracee TIDs to multiple rows --- volatility3/framework/plugins/linux/ptrace.py | 105 +++++++----------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index 1a37b8782e..a1964d9603 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -3,10 +3,9 @@ # import logging -from typing import List +from typing import List, Iterator -from volatility3.framework import renderers, interfaces, constants, objects -from volatility3.framework.constants.linux import PT_FLAGS +from volatility3.framework import renderers, interfaces from volatility3.framework.constants.architectures import LINUX_ARCHS from volatility3.framework.objects import utility from volatility3.framework.configuration import requirements @@ -17,7 +16,7 @@ class Ptrace(plugins.PluginInterface): - """Enumerates tracer and tracee tasks""" + """Enumerates ptrace's tracer and tracee tasks""" _required_framework_version = (2, 10, 0) _version = (1, 0, 0) @@ -36,85 +35,63 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] @classmethod - def enumerate_ptraced_tasks( + def enumerate_ptrace_tasks( cls, context: interfaces.context.ContextInterface, - symbol_table: str, - ): - vmlinux = context.modules[symbol_table] + vmlinux_module_name: str, + ) -> Iterator[interfaces.objects.ObjectInterface]: + """Enumerates ptrace's tracer and tracee tasks - tsk_struct_symname = vmlinux.symbol_table_name + constants.BANG + "task_struct" + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Yields: + A task_struct object + """ tasks = pslist.PsList.list_tasks( context, - symbol_table, + vmlinux_module_name, filter_func=pslist.PsList.create_pid_filter(), include_threads=True, ) for task in tasks: - tracing_tid_list = [ - int(task_being_traced.pid) - for task_being_traced in task.ptraced.to_list( - tsk_struct_symname, "ptrace_entry" - ) + if task.is_being_ptraced or task.is_ptracing: + yield task + + def _generator(self, vmlinux_module_name): + for task in self.enumerate_ptrace_tasks(self.context, vmlinux_module_name): + task_comm = utility.array_to_string(task.comm) + user_pid = task.tgid + user_tid = task.pid + tracer_tid = task.get_ptrace_tracer_tid() or renderers.NotAvailableValue() + tracee_tids = task.get_ptrace_tracee_tids() or [ + renderers.NotAvailableValue() ] - - if task.ptrace == 0 and not tracing_tid_list: - continue - - flags = ( - PT_FLAGS(task.ptrace).flags - if task.ptrace != 0 - else renderers.NotAvailableValue() - ) - - traced_by_tid = ( - task.parent.pid - if task.real_parent != task.parent - else renderers.NotAvailableValue() - ) - - tracing_tids = ",".join(map(str, tracing_tid_list)) - - yield task.comm, task.tgid, task.pid, traced_by_tid, tracing_tids, flags - - def _generator(self, symbol_table): - for fields in self.enumerate_ptraced_tasks(self.context, symbol_table): - yield (0, fields) - - @staticmethod - def format_fields_with_headers(headers, generator): - """Uses the headers type to cast the fields obtained from the generator""" - for level, fields in generator: - formatted_fields = [] - for header, field in zip(headers, fields): - header_type = header[1] - - if isinstance( - field, (header_type, interfaces.renderers.BaseAbsentValue) - ): - formatted_field = field - elif isinstance(field, objects.Array) and header_type is str: - formatted_field = utility.array_to_string(field) - else: - formatted_field = header_type(field) - - formatted_fields.append(formatted_field) - yield level, formatted_fields + flags = task.get_ptrace_tracee_flags() or renderers.NotAvailableValue() + + for tree, tracing_tid in enumerate(tracee_tids): + fields = [ + task_comm, + user_pid, + user_tid, + tracer_tid, + tracing_tid, + flags, + ] + yield (tree, fields) def run(self): - symbol_table = self.config["kernel"] + vmlinux_module_name = self.config["kernel"] headers = [ ("Process", str), ("PID", int), ("TID", int), ("Traced by TID", int), - ("Tracing TIDs", str), + ("Tracing TID", int), ("Flags", str), ] - return renderers.TreeGrid( - headers, - self.format_fields_with_headers(headers, self._generator(symbol_table)), - ) + return renderers.TreeGrid(headers, self._generator(vmlinux_module_name)) From aa959139409c22156cadb0b8fc91819f4d4f2c34 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 16:03:36 +1100 Subject: [PATCH 182/250] Linux: ptrace: improve header names and variables --- volatility3/framework/plugins/linux/ptrace.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index a1964d9603..e501f32948 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -72,16 +72,16 @@ def _generator(self, vmlinux_module_name): ] flags = task.get_ptrace_tracee_flags() or renderers.NotAvailableValue() - for tree, tracing_tid in enumerate(tracee_tids): + for level, tracee_tid in enumerate(tracee_tids): fields = [ task_comm, user_pid, user_tid, tracer_tid, - tracing_tid, + tracee_tid, flags, ] - yield (tree, fields) + yield (level, fields) def run(self): vmlinux_module_name = self.config["kernel"] @@ -90,8 +90,8 @@ def run(self): ("Process", str), ("PID", int), ("TID", int), - ("Traced by TID", int), - ("Tracing TID", int), + ("Tracer TID", int), + ("Tracee TID", int), ("Flags", str), ] return renderers.TreeGrid(headers, self._generator(vmlinux_module_name)) From 384042202e19fe5a4761daa74b76bed7fc70f55d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 18:37:32 +1100 Subject: [PATCH 183/250] fix black stable version issue with Python 3.8 --- .github/workflows/black.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 5adab32593..e29ab6f290 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -11,3 +11,5 @@ jobs: with: options: "--check --diff --verbose" src: "./volatility3" + # FIXME: Remove when Volatility3 minimum Python version is >3.8 + version: "24.8.0" From 6b5bef41336b3fbd623aca85499aa39410188fb0 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 21:27:29 +1100 Subject: [PATCH 184/250] Linux: ptrace: bumping version patch number to re-run the black linter with #1301 --- volatility3/framework/plugins/linux/ptrace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index e501f32948..34185f9128 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -19,7 +19,7 @@ class Ptrace(plugins.PluginInterface): """Enumerates ptrace's tracer and tracee tasks""" _required_framework_version = (2, 10, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 620dd4e67d971e97adab5daea523b60458bc5c35 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 8 Oct 2024 21:55:21 +1100 Subject: [PATCH 185/250] Linux: ptrace: version patch number back to zero --- volatility3/framework/plugins/linux/ptrace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index 34185f9128..e501f32948 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -19,7 +19,7 @@ class Ptrace(plugins.PluginInterface): """Enumerates ptrace's tracer and tracee tasks""" _required_framework_version = (2, 10, 0) - _version = (1, 0, 1) + _version = (1, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From e817e72bc2d82283ab2e942725f809ed31f5ca80 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 8 Oct 2024 22:52:44 +0100 Subject: [PATCH 186/250] Support hiding columns in the CLI --- volatility3/cli/__init__.py | 13 ++++ volatility3/cli/text_renderer.py | 92 +++++++++++++++++++++-------- volatility3/framework/exceptions.py | 4 ++ 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 50b5579afd..b4e993dacc 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -256,6 +256,14 @@ def run(self): default=[], action="append", ) + parser.add_argument( + "--hide-columns", + help="Case-insensitive space separated list of prefixes to determine which columns to hide in the output if provided", + default=None, + action="extend", + nargs="*", + type=str, + ) parser.set_defaults(**default_config) @@ -488,6 +496,7 @@ def run(self): grid = constructed.run() renderer = renderers[args.renderer]() renderer.filter = text_filter.CLIFilter(grid, args.filters) + renderer.column_hide_list = args.hide_columns renderer.render(grid) except exceptions.VolatilityException as excp: self.process_exceptions(excp) @@ -615,6 +624,10 @@ def process_exceptions(self, excp): caused_by = [ "A required python module is not installed (install the module and re-run)" ] + elif isinstance(excp, exceptions.RenderException): + general = "Volatility experienced an issue when rendering the output:" + detail = f"{excp}" + caused_by = ["An invalid renderer option, such as no visible columns"] else: general = "Volatility encountered an unexpected situation." detail = "" diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index da0cdf62a7..96970d3cef 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Tuple from volatility3.cli import text_filter -from volatility3.framework import interfaces, renderers +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -141,6 +141,30 @@ class CLIRenderer(interfaces.renderers.Renderer): name = "unnamed" structured_output = False filter: text_filter.CLIFilter = None + column_hide_list: list = None + + def ignored_columns( + self, + grid: interfaces.renderers.TreeGrid, + ) -> List[interfaces.renderers.Column]: + ignored_column_list = [] + if self.column_hide_list: + for column in grid.columns: + accept = True + for column_prefix in self.column_hide_list: + if column.name.lower().startswith(column_prefix.lower()): + accept = False + if not accept: + ignored_column_list.append(column) + elif self.column_hide_list is None: + return [] + + if len(ignored_column_list) == len(grid.columns): + raise exceptions.RenderException("No visible columns to render") + vollog.info( + f"Hiding columns: {[column.name for column in ignored_column_list]}" + ) + return ignored_column_list class QuickTextRenderer(CLIRenderer): @@ -173,9 +197,11 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout line = [] + ignore_columns = self.ignored_columns(grid) for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - line.append(f"{column.name}") + if column not in ignore_columns: + line.append(f"{column.name}") outfd.write("\n{}\n".format("\t".join(line))) def visitor(node: interfaces.renderers.TreeNode, accumulator): @@ -184,7 +210,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) - line.append(renderer(node.values[column_index])) + if column not in ignore_columns: + line.append(renderer(node.values[column_index])) if self.filter and self.filter.filter(line): return accumulator @@ -245,11 +272,13 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: grid: The TreeGrid object to render """ outfd = sys.stdout + ignore_columns = self.ignored_columns(grid) header_list = ["TreeDepth"] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - header_list.append(f"{column.name}") + if column not in ignore_columns: + header_list.append(f"{column.name}") writer = csv.DictWriter( outfd, header_list, lineterminator="\n", escapechar="\\" @@ -265,7 +294,10 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): column.type, self._type_renderers["default"] ) row[f"{column.name}"] = renderer(node.values[column_index]) - line.append(row[f"{column.name}"]) + if column not in ignore_columns: + line.append(row[f"{column.name}"]) + else: + del row[f"{column.name}"] if self.filter and self.filter.filter(line): return accumulator @@ -303,6 +335,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: sys.stderr.write("Formatting...\n") + ignore_columns = self.ignored_columns(grid) display_alignment = ">" column_separator = " | " @@ -335,7 +368,8 @@ def visitor( max_column_widths[column.name] = max( max_column_widths.get(column.name, len(column.name)), field_width ) - line[column] = data.split("\n") + if column not in ignore_columns: + line[column] = data.split("\n") rendered_line.append(data) if self.filter and self.filter.filter(rendered_line): @@ -354,43 +388,49 @@ def visitor( format_string_list = [ "{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}" ] + column_offset = 0 for column_index, column in enumerate(grid.columns): - format_string_list.append( - "{" - + str(column_index + 1) - + ":" - + display_alignment - + str(max_column_widths[column.name]) - + "s}" - ) + if column not in ignore_columns: + format_string_list.append( + "{" + + str(column_index - column_offset + 1) + + ":" + + display_alignment + + str(max_column_widths[column.name]) + + "s}" + ) + else: + column_offset += 1 format_string = column_separator.join(format_string_list) + "\n" - column_titles = [""] + [column.name for column in grid.columns] + column_titles = [""] + [ + column.name for column in grid.columns if column not in ignore_columns + ] + outfd.write(format_string.format(*column_titles)) for depth, line in final_output: nums_line = max([len(line[column]) for column in line]) for column in line: - line[column] = line[column] + ([""] * (nums_line - len(line[column]))) + if column in ignore_columns: + del line[column] + else: + line[column] = line[column] + ( + [""] * (nums_line - len(line[column])) + ) for index in range(nums_line): if index == 0: outfd.write( format_string.format( "*" * depth, - *[ - self.tab_stop(line[column][index]) - for column in grid.columns - ], + *[self.tab_stop(line[column][index]) for column in line], ) ) else: outfd.write( format_string.format( " " * depth, - *[ - self.tab_stop(line[column][index]) - for column in grid.columns - ], + *[self.tab_stop(line[column][index]) for column in line], ) ) @@ -436,6 +476,8 @@ def render(self, grid: interfaces.renderers.TreeGrid): List[interfaces.renderers.TreeNode], ] = ({}, []) + ignore_columns = self.ignored_columns(grid) + def visitor( node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], @@ -445,6 +487,8 @@ def visitor( node_dict: Dict[str, Any] = {"__children": []} line = [] for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index f8701683b0..c44fb4f2e2 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -126,3 +126,7 @@ def __init__(self, url: str, *args) -> None: def __str__(self): return f"Volatility 3 is offline: unable to access {self._url}" + + +class RenderException(VolatilityException): + """Thrown if there is an error during rendering""" From 57ef3f587eae3bb0eb46e760e64ebdc9fbd36394 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Mon, 30 Sep 2024 14:51:42 -0500 Subject: [PATCH 187/250] Windows: Adds scheduled tasks plugin This adds a plugin 'ScheduledTasks' that can decode binary-encoded scheduled tasks from the Windows registry's SOFTWARE hive using a custom reader that extends the `io.BytesIO` class. Decoding operations are intended to be as fault tolerant as possible, swallowing exceptions and returning `None` to account for smear or missing data. Because each task can have mulitple triggers and multiple actions, a single entry is generated for each trigger + action pair. In the event that the either the actions could not be parsed or the triggers could not be parsed due to missing or smeared data, an entry will still be generated using the available information from the other registry value, since trigger and action data is stored separately. Much more information is decoded than is rendered, this was done intentionally to avoid overpopulating the TreeGrid with less pertinent data and to avoid an explosion of trigger and action-specific fields that may not apply to most other entries. --- volatility3/framework/layers/registry.py | 3 + .../plugins/windows/scheduled_tasks.py | 1796 +++++++++++++++++ volatility3/framework/renderers/conversion.py | 21 + 3 files changed, 1820 insertions(+) create mode 100644 volatility3/framework/plugins/windows/scheduled_tasks.py diff --git a/volatility3/framework/layers/registry.py b/volatility3/framework/layers/registry.py index 471d19f68f..6098328867 100644 --- a/volatility3/framework/layers/registry.py +++ b/volatility3/framework/layers/registry.py @@ -170,6 +170,9 @@ def get_key( return_list specifies whether the return result will be a single node (default) or a list of nodes from root to the current node (if return_list is true). + + Raises RegistryFormatException if an invalid structure is encountered + Raises KeyError if the key is not found """ root_node = self.get_node(self.root_cell_offset) if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"): diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py new file mode 100644 index 0000000000..6d3ee586dd --- /dev/null +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -0,0 +1,1796 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import base64 +import binascii +import dataclasses +import datetime +import enum +import io +import itertools +import logging +import struct +import traceback +import unittest +from typing import Dict, Iterator, List, Optional, Tuple, Union + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import registry +from volatility3.framework.renderers import conversion +from volatility3.framework.symbols.windows.extensions import registry as reg_extensions +from volatility3.plugins import timeliner +from volatility3.plugins.windows.registry import hivelist + +vollog = logging.getLogger(__name__) + +# Reference: https://cyber.wtf/2022/06/01/windows-registry-analysis-todays-episode-tasks/ + + +class TimeMode(enum.Enum): + """ + Enumeration containing the different time modes that a 'Time' trigger can be configured to run in. + """ + + Once = "Once" + + # run at and repeat every days + Daily = "Daily" + + # run on days of week <(data2 as day_of_week bitmap)> every weeks starting at + Weekly = "Weekly" + + # run in months <(data3 as months bitmap> on days <(data2:data1 as day in month bitmap)> + # starting at + DaysInMonths = "Days In Months" + + # run in months <(data3 as months bitmap> in weeks <(data2 as week bitmap)> + # on days <(data1 as day_of_week bitmap)> starting at + DaysInWeeksInMonths = "Days In Weeks in Months" + + Unknown = "Unknown" + + +class ActionType(enum.Enum): + """ + Enumeration that maps action types to their magic number encodings + """ + + Exe = 0x6666 + ComHandler = 0x7777 + Email = 0x8888 + MessageBox = 0x9999 + + +class TriggerType(enum.Enum): + """ + Enumeration that maps trigger types to their magic number encodings + """ + + WindowsNotificationFacility = 0x6666 + Session = 0x7777 + Registration = 0x8888 + Logon = 0xAAAA + Event = 0xCCCC + Time = 0xDDDD + Idle = 0xEEEE + Boot = 0xFFFF + + +class Weekday(enum.Enum): + """ + Enumeration that contains bitwise values for days of the week. + """ + + Sunday = 0x1 + Monday = 0x2 + Tuesday = 0x4 + Wednesday = 0x8 + Thursday = 0x10 + Friday = 0x20 + Saturday = 0x40 + + +class Months(enum.Enum): + """ + Enumeration that contains bitwise values for months of the year. + """ + + January = 0x1 + February = 0x2 + March = 0x4 + April = 0x8 + May = 0x10 + June = 0x20 + July = 0x40 + August = 0x80 + September = 0x100 + October = 0x200 + November = 0x400 + December = 0x800 + + +class SidType(enum.Enum): + """ + Enumeration that maps SID types to their encoded integer values + """ + + User = 1 + Group = 2 + Domain = 3 + Alias = 4 + WellKnownGroup = 5 + DeletedAccount = 6 + Invalid = 7 + Unknown = 8 + Computer = 9 + Label = 10 + LogonSession = 11 + + +@dataclasses.dataclass +class TaskSchedulerTime: + """ + A class containing datetime information about when a task will run + """ + + is_localized: bool + filetime: Optional[datetime.datetime] + + +@dataclasses.dataclass +class TaskSchedulerTimePeriod: + """ + Class containing information delimiting time periods within scheduled tasks. + """ + + years: int + months: int + weeks: int + days: int + hours: int + minutes: int + seconds: int + + +JOB_BUCKET_FLAGS = { + 0x2: "Run only if idle", + 0x4: "Restart on idle", + 0x8: "Stop on idle end", + 0x10: "Disallow start if on batteries", + 0x20: "Stop if going on batteries", + 0x40: "Start when available", + 0x80: "Run only if network available", + 0x100: "Allow start on demand", + 0x200: "Wake to run", + 0x400: "Execute parallel", + 0x800: "Execute stop existing", + 0x1000: "Execute queue", + 0x2000: "Execute ignore new", + 0x4000: "Logon type s4u", + 0x10000: "Logon type InteractiveToken", + 0x40000: "Logon type Password", + 0x80000: "Logon type InteractiveTokenOrPassword", + 0x400000: "Enabled", + 0x800000: "Hidden", + 0x1000000: "Runlevel highest available", + 0x2000000: "Task", + 0x4000000: "Version", + 0x8000000: "Token SID type none", + 0x10000000: "Token SID type unrestricted", + 0x20000000: "Interval", + 0x40000000: "Allow hard terminate", +} + +NULL = "\u0000" + + +class _ScheduledTasksReader(io.BytesIO): + + def read_task_scheduler_time(self) -> Optional[Tuple[bool, datetime.datetime]]: + is_localized = bool(self.read_aligned_u1()) + filetime = self.decode_filetime() + if filetime is None: + return None + return is_localized, filetime + + def read_filetime(self) -> Optional[datetime.datetime]: + return datetime.datetime.now() + + def seek_relative(self, offset) -> int: + return self.seek(offset, io.SEEK_CUR) + + def read_bool(self, aligned=False) -> Optional[bool]: + try: + val = struct.unpack("?", self.read(1))[0] + if aligned: + self.seek(7) + return val + except struct.error: + return None + + def decode_filetime(self) -> Optional[datetime.datetime]: + filetime = self.read_u8() + if filetime is None: + return None + + if filetime == 0 or filetime == 0xFFFFFFFFFFFFFFFF: + return None + filetime = conversion.wintime_to_datetime(filetime) + if isinstance(filetime, datetime.datetime): + return filetime + else: + return None + + def _read_uint( + self, size: int, format: str, aligned: bool = False + ) -> Optional[int]: + try: + val = struct.unpack(format, self.read(size))[0] + if aligned: + self.seek(8 - size, io.SEEK_CUR) + return val + except struct.error: + return None + + def read_aligned_u1(self) -> Optional[int]: + return self._read_uint(1, "B", True) + + def read_u2(self) -> Optional[int]: + return self._read_uint(2, " Optional[int]: + return self._read_uint(2, " Optional[int]: + return self._read_uint(4, " Optional[int]: + return self._read_uint(8, " Optional[int]: + return self._read_uint(4, " Optional[bytes]: + count = self.read_u4() if not aligned else self.read_aligned_u4() + if count is None: + return None + data = self.read(count) + if aligned: + self.seek((8 - (count % 8)) % 8, io.SEEK_CUR) + return data + + def read_bstring(self, aligned=False) -> Optional[str]: + size = self.read_u4() if not aligned else self.read_aligned_u4() + if size is None: + return None + try: + raw = self.read(size) + val = raw.decode("utf-16le", errors="replace").rstrip(NULL) or None + except UnicodeDecodeError: + val = None + + if aligned: + self.seek((8 - (size % 8)) % 8, io.SEEK_CUR) + + return val + + def read_aligned_bstring_expand_sz(self) -> Optional[str]: + # type: () -> Optional[str] + sz = self.read_aligned_u4() + if sz is None: + return None + byte_count = sz * 2 + 2 + + if sz == 0: + return None + + try: + content = self.read(byte_count).decode("utf-16le") + except UnicodeDecodeError: + content = None + + self.seek((8 - (byte_count % 8)) % 8, io.SEEK_CUR) + return content.rstrip("\x00") if content is not None else None + + def read_tstimeperiod(self) -> Optional[TaskSchedulerTimePeriod]: + values = ( + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + self.read_u2(), + ) + + if any(value is None for value in values): + return None + + return TaskSchedulerTimePeriod(*values) + + +def _build_guid_name_map(key: reg_extensions.CM_KEY_NODE) -> Dict[str, str]: + mapping = {} + task_id_value = None + for value in key.get_values(): + try: + if value.get_name() == "Id": + task_id_value = value + break + except exceptions.InvalidAddressException: + continue + + if ( + task_id_value is not None + and task_id_value.get_type() == reg_extensions.RegValueTypes.REG_SZ + ): + try: + id_str = task_id_value.decode_data() + except exceptions.InvalidAddressException: + id_str = None + + if isinstance(id_str, bytes): + mapping[id_str.decode("utf-16le", errors="replace").rstrip(NULL)] = str( + key.get_name() + ) + + for subkey in key.get_subkeys(): + mapping.update(_build_guid_name_map(subkey)) + return mapping + + +@dataclasses.dataclass +class TaskAction: + action_type: ActionType + action: str + action_args: Optional[str] + working_directory: Optional[str] + + @classmethod + def decode_messagebox_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + caption, content = reader.read_bstring(), reader.read_bstring() + return cls( + ActionType.MessageBox, + f'"{caption or ""}": {content or ""}', + None, + None, + ) + + @classmethod + def _decode_exe_action( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskAction"]: + command = reader.read_bstring() + args = reader.read_bstring() + if command is None or args is None: + return None + + workdir = reader.read_bstring() + if version == 3: + _flags = reader.read_u2() + + return cls(ActionType.Exe, command, args, workdir) + + @classmethod + def _decode_email_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + props = { + "From": reader.read_bstring(), + "To": reader.read_bstring(), + "Cc": reader.read_bstring(), + "Bcc": reader.read_bstring(), + "Reply_to": reader.read_bstring(), + "Server": reader.read_bstring(), + "Subject": reader.read_bstring(), + "Body": reader.read_bstring(), + } + + num_attachment_filenames = reader.read_u4() + if num_attachment_filenames is not None: + + attachment_filenames = [ + reader.read_bstring() for _ in range(num_attachment_filenames) + ] + + props["Attachments"] = ( + "<" + + ", ".join( + filename + for filename in attachment_filenames + if filename is not None + ) + + ">" + ) + + num_headers = reader.read_u4() + if num_headers is not None: + headers = [ + (reader.read_bstring(), reader.read_bstring()) + for _ in range(num_headers) + ] + + props["Headers"] = ( + "<" + + ", ".join( + f"{field}: {value}" + for field, value in headers + if field is not None and value is not None + ) + + ">" + ) + + cls( + ActionType.Email, + ", ".join( + f"{key}: {value}" for key, value in props.items() if value is not None + ), + None, + None, + ) + + @classmethod + def _decode_comhandler_action( + cls, reader: _ScheduledTasksReader + ) -> Optional["TaskAction"]: + guid_raw = reader.read(16) + if not guid_raw and len(guid_raw) == 16: + return None + clsid = conversion.windows_bytes_to_guid(guid_raw) + args = reader.read_bstring() + + return cls(ActionType.ComHandler, clsid, args, None) + + +@dataclasses.dataclass +class _ScheduledTaskEntry: + name: Union[str, interfaces.renderers.BaseAbsentValue] + principal_id: Union[str, interfaces.renderers.BaseAbsentValue] + display_name: Union[str, interfaces.renderers.BaseAbsentValue] + enabled: Union[bool, interfaces.renderers.BaseAbsentValue] + creation_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + last_run_time: Union[datetime.datetime, interfaces.renderers.BaseAbsentValue] + last_successful_run_time: Union[ + datetime.datetime, interfaces.renderers.BaseAbsentValue + ] + trigger_type: Union[str, interfaces.renderers.BaseAbsentValue] + trigger_description: Union[str, interfaces.renderers.BaseAbsentValue] + action_type: Union[str, interfaces.renderers.BaseAbsentValue] + action_description: Union[str, interfaces.renderers.BaseAbsentValue] + action_args: Union[str, interfaces.renderers.BaseAbsentValue] + action_context: Union[str, interfaces.renderers.BaseAbsentValue] + working_directory: Union[str, interfaces.renderers.BaseAbsentValue] + guid: str + + +@dataclasses.dataclass +class _JobSchedule: + start_boundary: Optional[datetime.datetime] + end_boundary: Optional[datetime.datetime] + repetition_interval_secs: Optional[int] + repetition_duration_secs: Optional[int] + execution_time_limit_secs: Optional[int] + mode: Optional[TimeMode] + data1: Optional[int] + data2: Optional[int] + data3: Optional[int] + stop_tasks_at_duration_end: Optional[int] + is_enabled: Optional[bool] + max_delay_seconds: Optional[int] + + def get_description(self) -> Optional[str]: + if self.mode == TimeMode.Once: + return "Run one time starting at {}".format( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ) + + elif self.mode == TimeMode.Daily: + if self.data1 is None: + return None + return "Run at {} and repeat every {} days".format( + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + self.data1, + ) + + elif self.mode == TimeMode.Weekly: + if self.data2 is None: + return None + + days = [k.name for k in Weekday if k.value & self.data2] + return "Run on {} every {} weeks starting at {}".format( + ", ".join(days), + self.data1, + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + elif self.mode == TimeMode.DaysInMonths: + if self.data2 is None or self.data1 is None or self.data3 is None: + return None + months = [month.name for month in Months if month.value & self.data3] + days_bitmap = (self.data2 << 16) + self.data1 + days = [str(v + 1) for v in range(31) if (1 << v) & days_bitmap] + return "Run in months {} on days {} starting at {}".format( + ", ".join(months), + ", ".join(days), + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + elif self.mode == TimeMode.DaysInWeeksInMonths: + if self.data1 is None or self.data2 is None or self.data3 is None: + return None + + months = [month.name for month in Months if month.value & self.data3] + weeks = [str(v + 1) for v in range(5) if (v << 1) & self.data2] + days = [day.name for day in Weekday if day.value & self.data1] + return "Run in months {} in weeks {} on days {} starting at {}".format( + ", ".join(months), + ", ".join(weeks), + ", ".join(days), + ( + self.start_boundary.isoformat() + if self.start_boundary is not None + else "" + ), + ) + else: + return None + + @classmethod + def decode(cls, reader: _ScheduledTasksReader) -> Optional["_JobSchedule"]: + start_boundary = reader.read_task_scheduler_time() + end_boundary = reader.read_task_scheduler_time() + + _ = reader.read_task_scheduler_time() + repetition_interval_secs = reader.read_u4() + repetition_duration_secs = reader.read_u4() + execution_time_limit_secs = reader.read_u4() + mode_index = reader.read_u4() + try: + if mode_index is not None: + mode = list(TimeMode)[mode_index] + else: + mode = None + except IndexError: + mode = TimeMode.Unknown + data1 = reader.read_u2() + data2 = reader.read_u2() + data3 = reader.read_u2() + + reader.seek(2, io.SEEK_CUR) # pad + stop_tasks_at_duration_end = reader.read_bool() + is_enabled = reader.read_bool() + reader.seek(6, io.SEEK_CUR) # pad (2) + unknown (4) + max_delay_seconds = reader.read_u4() + reader.seek(4, io.SEEK_CUR) # pad + + return cls( + start_boundary[1] if start_boundary is not None else None, + end_boundary[1] if end_boundary is not None else None, + repetition_interval_secs, + repetition_duration_secs, + execution_time_limit_secs, + mode, + data1, + data2, + data3, + stop_tasks_at_duration_end, + is_enabled, + max_delay_seconds, + ) + + +def decode_sid(data: bytes) -> Optional[str]: + """ + Decodes a windows SID from variable-length raw bytes + + Returns the string representation of the SID if decoding was successful, or None + if the data could not be parsed due to an insufficent number of bytes. + """ + try: + revision, subid_count, id_authority = struct.unpack( + ">BBQ", data[:2] + b"\x00\x00" + data[2:8] + ) + subauthorities = struct.unpack( + "<" + "I" * subid_count, data[8 : 8 + subid_count * 4] + ) + sid_string = "S-" + "-".join( + [str(item) for item in [revision, id_authority] + list(subauthorities)] + ) + except struct.error: + return None + + return sid_string + + +@dataclasses.dataclass +class UserInfo: + sid_type: Optional[SidType] + sid: Optional[str] + username: Optional[str] + + @classmethod + def _decode(cls, reader: _ScheduledTasksReader) -> Optional["UserInfo"]: + skip_user = reader.read_aligned_u1() != 0 + if not skip_user: + skip_sid = reader.read_aligned_u1() != 0 + else: + skip_sid = None + + sid_type = None + sid = None + if not skip_user and not skip_sid: + try: + sid_type = SidType(reader.read_aligned_u4()) + except ValueError: + sid_type = SidType.Unknown + + sid_raw = reader.read_buffer(aligned=True) + if sid_raw is None: + return None + sid = decode_sid(sid_raw) + + username = reader.read_bstring(aligned=True) if not skip_user else None + + return UserInfo(sid_type, sid, username) + + +@dataclasses.dataclass +class OptionalSettings: + IdleDurationSeconds: int + idleWaitTimeoutSeconds: int + ExecutionTimeLimitSeconds: int + DeleteExpiredTaskAfter: int + Priority: int + RestartOnFailureDelay: int + RestartOnFailureRetries: int + NetworkId: bytes + Privileges: Optional[List[str]] + Periodicity: Optional[TaskSchedulerTimePeriod] + Deadline: Optional[TaskSchedulerTimePeriod] + Exclusive: Optional[bool] + + @classmethod + def _decode(cls, reader: _ScheduledTasksReader) -> Optional["OptionalSettings"]: + LEN_WITH_PRIVILEGES = 0x38 + LEN_WITH_TIME_PERIODS = 0x58 + length = reader.read_aligned_u4() + if length == 0: + return None + + base_values = ( + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + reader.read_u4(), + binascii.hexlify(reader.read(16)), + ) + + if any(value is None for value in base_values): + return None + + reader.seek(4, io.SEEK_CUR) # padding + + privileges = None + periodicity = None + deadline = None + exclusive = None + if length == LEN_WITH_PRIVILEGES or length == LEN_WITH_TIME_PERIODS: + privileges_raw = reader.read_u8() + if privileges_raw is None: + return None + privileges = [ + priv.name for priv in Privileges if priv.value & privileges_raw + ] + if length == LEN_WITH_TIME_PERIODS: + periodicity = reader.read_tstimeperiod() + deadline = reader.read_tstimeperiod() + exclusive = reader.read_bool() + reader.seek(3, io.SEEK_CUR) # padding + + return OptionalSettings( + *base_values, privileges, periodicity, deadline, exclusive + ) + + +@dataclasses.dataclass +class JobBucket: + flags: List[str] + crc32: int + principal_id: Optional[str] + display_name: Optional[str] + user_info: Optional[UserInfo] + optional_settings: Optional[OptionalSettings] + + @classmethod + def _decode( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["JobBucket"]: + flags_raw = reader.read_aligned_u4() + if flags_raw is None: + return None + flags = [y for x, y in JOB_BUCKET_FLAGS.items() if x & flags_raw] + crc32 = reader.read_aligned_u4() + if crc32 is None: + return None + + principal_id = None + display_name = None + if version >= 0x16: + principal_id = reader.read_bstring(aligned=True) + if version >= 0x17: + display_name = reader.read_bstring(aligned=True) + + user_info = UserInfo._decode(reader) + optional_settings = OptionalSettings._decode(reader) + + return JobBucket( + flags, crc32, principal_id, display_name, user_info, optional_settings + ) + + +class Privileges(enum.Enum): + SeCreateTokenPrivilege = 0x4 + SeAssignPrimaryTokenPrivilege = 0x8 + SeLockMemoryPrivilege = 0x10 + SeIncreaseQuotaPrivilege = 0x20 + SeMachineAccountPrivilege = 0x40 + SeTcbPrivilege = 0x80 + SeSecurityPrivilege = 0x100 + SeTakeOwnershipPrivilege = 0x200 + SeLoadDriverPrivilege = 0x400 + SeSystemProfilePrivilege = 0x800 + SeSystemtimePrivilege = 0x1000 + SeProfileSingleProcessPrivilege = 0x2000 + SeIncreaseBasePriorityPrivilege = 0x4000 + SeCreatePagefilePrivilege = 0x8000 + SeCreatePermanentPrivilege = 0x10000 + SeBackupPrivilege = 0x20000 + SeRestorePrivilege = 0x40000 + SeShutdownPrivilege = 0x80000 + SeDebugPrivilege = 0x100000 + SeAuditPrivilege = 0x200000 + SeSystemEnvironmentPrivilege = 0x400000 + SeChangeNotifyPrivilege = 0x800000 + SeRemoteShutdownPrivilege = 0x1000000 + SeUndockPrivilege = 0x2000000 + SeSyncAgentPrivilege = 0x4000000 + SeEnableDelegationPrivilege = 0x8000000 + SeManageVolumePrivilege = 0x10000000 + SeImpersonatePrivilege = 0x20000000 + SeCreateGlobalPrivilege = 0x40000000 + SeTrustedCredManAccessPrivilege = 0x80000000 + SeRelabelPrivilege = 0x100000000 + SeIncreaseWorkingSetPrivilege = 0x200000000 + SeTimeZonePrivilege = 0x400000000 + SeCreateSymbolicLinkPrivilege = 0x800000000 + SeDelegateSessionUserImpersonatePrivilege = 0x1000000000 + + +class SessionState(enum.Enum): + ConsoleConnect = 1 + ConsoleDisconnect = 2 + RemoteConnect = 3 + RemoteDisconnect = 4 + SessionLock = 5 + SessionUnlock = 6 + Unknown = "Unknown" + + +@dataclasses.dataclass +class TaskTrigger: + start_boundary: Optional[datetime.datetime] + end_boundary: Optional[datetime.datetime] + repetition_interval_seconds: Optional[int] + enabled: Optional[bool] + trigger_type: TriggerType + description: Optional[str] + + @classmethod + def _decode_generic_trigger( + cls, reader: _ScheduledTasksReader, version: int, trigger_type: TriggerType + ) -> Optional["TaskTrigger"]: + start_boundary = reader.read_task_scheduler_time() + end_boundary = reader.read_task_scheduler_time() + + _ = reader.read_u4() # delay seconds + _ = reader.read_u4() # timeout seconds + + repetition_interval_secs = reader.read_u4() + _ = reader.read_u4() # reptition duration seconds + _ = reader.read_u4() # repetition duration seconds 2 + + _ = reader.read_bool() # stop at duration end + reader.seek(3, io.SEEK_CUR) + trigger_enabled = bool(reader.read_aligned_u1()) + reader.seek(8, io.SEEK_CUR) # unknown field + + if version >= 0x16: + cur = reader.tell() + _ = reader.read_bstring() # trigger id + reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block + + return cls( + start_boundary[1] if start_boundary is not None else None, + end_boundary[1] if end_boundary is not None else None, + repetition_interval_secs, + trigger_enabled, + trigger_type, + f"{trigger_type.name} trigger", + ) + + @classmethod + def _decode_logon_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Logon) + if base is None: + return None + + user = UserInfo._decode(reader) + if user is not None and user.username is not None: + base.description = f"{user.username}: {user.sid} ({user.sid_type})" + + return base + + @classmethod + def _decode_session_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Session) + if base is None: + return None + session_type_raw = reader.read_u4() + reader.seek(4, io.SEEK_CUR) + + try: + session_type = SessionState(session_type_raw) + except ValueError: + session_type = SessionState.Unknown + + user_info = UserInfo._decode(reader) + if user_info is not None and user_info.username is not None: + base.description = f"{session_type.name} for user {user_info.username}" + else: + base.description = session_type.name + + return base + + @classmethod + def _decode_time_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + job_schedule = _JobSchedule.decode(reader) + if job_schedule is None: + return None + + if version >= 0x16: + cur = reader.tell() + _ = reader.read_bstring() # trigger id + reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block + + return cls( + job_schedule.start_boundary, + job_schedule.end_boundary, + job_schedule.repetition_interval_secs, + job_schedule.is_enabled, + TriggerType.Time, + job_schedule.get_description() or None, + ) + + @classmethod + def _decode_event_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger(reader, version, TriggerType.Event) + if base is None: + return base + + subscription = reader.read_aligned_bstring_expand_sz() + reader.seek(8, io.SEEK_CUR) # 2 4-byte unknown fields + reader.read_aligned_bstring_expand_sz() # another unknown field + len_value_queries = reader.read_aligned_u4() + + if len_value_queries is None: + return base + + queries = [ + ( + reader.read_aligned_bstring_expand_sz(), + reader.read_aligned_bstring_expand_sz(), + ) + for _ in range(len_value_queries) + ] + valid = [(k, v) for (k, v) in queries if k is not None and v is not None] + if base.description is None: + base.description = "Event Trigger" + base.description += f": Subscription: {subscription}, Queries: {str(valid)}" + return base + + @classmethod + def _decode_boot_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Boot) + + @classmethod + def _decode_wnf_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + base = cls._decode_generic_trigger( + reader, version, TriggerType.WindowsNotificationFacility + ) + if base is None: + return None + + state_name = binascii.hexlify(reader.read(8)).decode("ascii") + datalen = reader.read_aligned_u4() + _ = base64.b64encode(reader.read(datalen)) # state binary data + base.description = f"WNF state {state_name}" + return base + + @classmethod + def _decode_idle_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Logon) + + @classmethod + def _decode_registration_trigger( + cls, reader: _ScheduledTasksReader, version: int + ) -> Optional["TaskTrigger"]: + return cls._decode_generic_trigger(reader, version, TriggerType.Logon) + + +@dataclasses.dataclass +class TriggerSet: + job_bucket: JobBucket + triggers: List[TaskTrigger] + + @classmethod + def decode(cls, data) -> Optional["TriggerSet"]: + reader = _ScheduledTasksReader(data) + + version = reader.read_aligned_u1() + _ = reader.read_task_scheduler_time() # start boundary + _ = reader.read_task_scheduler_time() # end_boundary + + if version is None: + return None + + job_bucket = JobBucket._decode(reader, version) + if job_bucket is None: + return None + + triggers = [] + + while True: + magic = reader.read_aligned_u4() + if magic is None: + break + try: + trigger_type = TriggerType(magic) + except ValueError: + vollog.warning(f"Invalid trigger magic {hex(magic)}") + break + + if trigger_type == TriggerType.Logon: + trigger = TaskTrigger._decode_logon_trigger(reader, version) + elif trigger_type == TriggerType.Session: + trigger = TaskTrigger._decode_session_trigger(reader, version) + elif trigger_type == TriggerType.WindowsNotificationFacility: + trigger = TaskTrigger._decode_wnf_trigger(reader, version) + elif trigger_type == TriggerType.Boot: + trigger = TaskTrigger._decode_boot_trigger(reader, version) + elif trigger_type == TriggerType.Registration: + trigger = TaskTrigger._decode_registration_trigger(reader, version) + elif trigger_type == TriggerType.Event: + trigger = TaskTrigger._decode_event_trigger(reader, version) + elif trigger_type == TriggerType.Idle: + trigger = TaskTrigger._decode_idle_trigger(reader, version) + elif trigger_type == TriggerType.Time: + trigger = TaskTrigger._decode_time_trigger(reader, version) + else: + vollog.warning( + f"Invalid trigger magic {hex(magic)} encountered at offset {hex(reader.tell() - 8)}, stopping parsing" + ) + break + triggers.append(trigger) + + return cls(job_bucket, triggers) + + +@dataclasses.dataclass +class ActionSet: + actions: List[TaskAction] + context: Optional[str] + + @classmethod + def decode(cls, data: bytes) -> Optional["ActionSet"]: + reader = _ScheduledTasksReader(data) + actions = [] + + version = reader.read_u2() + if version is None: + return None + + if version in [2, 3]: + action_context = reader.read_bstring() + else: + action_context = None + + while True: + magic = reader.read_u2() + if magic is None: + break + + _ = ( + reader.read_bstring() + ) # action identifier, usually (but not always) empty + + if magic == ActionType.Email.value: + action = TaskAction._decode_email_action(reader) + elif magic == ActionType.Exe.value: + action = TaskAction._decode_exe_action(reader, version) + elif magic == ActionType.ComHandler.value: + action = TaskAction._decode_comhandler_action(reader) + elif magic == ActionType.MessageBox.value: + action = TaskAction.decode_messagebox_action(reader) + else: + break + actions.append(action) + + return cls(actions, action_context) + + +@dataclasses.dataclass +class DynamicInfo: + """ + Contains information about execution history for this task, + including timestamps and the last error code + """ + + creation_time: Optional[datetime.datetime] + last_run_time: Optional[datetime.datetime] + last_successful_run_time: Optional[datetime.datetime] + last_error_code: Optional[int] + + @classmethod + def decode(cls, data: bytes) -> Optional["DynamicInfo"]: + """ + Decodes a DynamicInfo structure from RegBin value data. + Raises a `ScheduledTaskDecodingError` if the magic bytes are invalid, but otherwise + attempts to decode as much as possible without returning an error. + """ + DYNAMICINFO_MAGIC = 3 + + reader = _ScheduledTasksReader(data) + magic = reader.read_u4() + if magic != DYNAMICINFO_MAGIC: + return None + + creation_time = reader.decode_filetime() + last_run_time = reader.decode_filetime() + + reader.seek(4, io.SEEK_CUR) # deprecated field 'TaskState' + + last_error_code = reader.read_u4() + last_success_time = reader.decode_filetime() + + vollog.debug((creation_time, last_run_time, last_success_time)) + + return cls( + last_run_time, + creation_time, + last_success_time, + last_error_code, + ) + + +class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): + """Decodes scheduled task information from the Windows registry, including + information about triggers, actions, run times, and creation times. + """ + + _required_framework_version = (2, 11, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel33", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + ] + + def generate_timeline( + self, + ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: + for _, task in self._generator(): + if isinstance(task.last_run_time, datetime.datetime): + yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", timeliner.TimeLinerType.ACCESSED, task.last_run_time + if isinstance(task.last_successful_run_time, datetime.datetime): + yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", timeliner.TimeLinerType.ACCESSED, task.last_successful_run_time + if isinstance(task.creation_time, datetime.datetime): + yield f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", timeliner.TimeLinerType.CREATED, task.creation_time + + @classmethod + def get_software_hive( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + kernel: interfaces.context.ModuleInterface, + ) -> Optional[registry.RegistryHive]: + """Retrieves the `Amcache.hve` registry hive from the kernel module, if it can be located.""" + return next( + hivelist.HiveList.list_hives( + context=context, + base_config_path=interfaces.configuration.path_join( + config_path, "hivelist" + ), + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="SOFTWARE", + ), + None, + ) + + @classmethod + def parse_actions_value( + cls, actions_value: reg_extensions.CM_KEY_VALUE + ) -> Optional[ActionSet]: + """Parses File entries from the Windows 8 `Root\\File` key. + + :param programs_key: The `Root\\File` registry key. + + :return: An iterator of tuples, where the first member is the program ID string for + correlating `Root\\Program` entries, and the second member is the `AmcacheEntry`. + """ + try: + data = actions_value.decode_data() + except exceptions.InvalidAddressException: + data = None + + if not isinstance(data, bytes): + return None + + return ActionSet.decode(data) + + @classmethod + def parse_triggers_value( + cls, triggers_value: reg_extensions.CM_KEY_VALUE + ) -> Optional[TriggerSet]: + try: + data = triggers_value.decode_data() + except exceptions.InvalidAddressException: + data = None + + if not isinstance(data, bytes): + return None + + return TriggerSet.decode(data) + + @classmethod + def parse_dynamic_info_value( + cls, dyn_info_value: reg_extensions.CM_KEY_VALUE + ) -> Optional[DynamicInfo]: + + try: + data = dyn_info_value.decode_data() + except exceptions.InvalidAddressException: + data = None + + if not isinstance(data, bytes): + return None + + return DynamicInfo.decode(data) + + @classmethod + def _get_task_keys( + cls, software_hive: reg_extensions.RegistryHive + ) -> Tuple[ + Optional[reg_extensions.CM_KEY_NODE], Optional[reg_extensions.CM_KEY_NODE] + ]: + try: + task_key = software_hive.get_key( + "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tasks" + ) + except (KeyError, registry.RegistryFormatException): + task_key = None + + try: + task_tree = software_hive.get_key( + "Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree" + ) + except (KeyError, registry.RegistryFormatException): + task_tree = None + + return (task_key, task_tree) # type: ignore + + @classmethod + def _parse_task_key( + cls, key: reg_extensions.CM_KEY_NODE, guid_mapping: Dict[str, str] + ) -> Iterator[_ScheduledTaskEntry]: + values = {} + for value in key.get_values(): + try: + name = str(value.get_name()) + except exceptions.InvalidAddressException: + continue + + if name in ["Actions", "Triggers", "DynamicInfo"]: + values[name] = value + + task_name = guid_mapping.get(str(key.get_name()), renderers.NotAvailableValue()) + + try: + action_set = cls.parse_actions_value(values["Actions"]) + except KeyError: + vollog.debug("Failed to get Actions value") + action_set = None + + try: + triggers_value = values["Triggers"] + trigger_set = cls.parse_triggers_value(triggers_value) + except KeyError: + vollog.debug("Failed to get Triggers value") + trigger_set = None + + if trigger_set is not None: + vollog.debug("Parsed triggers successfully") + + principal_id = ( + trigger_set.job_bucket.principal_id or renderers.NotAvailableValue() + ) + display_name = ( + trigger_set.job_bucket.display_name or renderers.NotAvailableValue() + ) + else: + vollog.debug("Failed to parse triggers") + + principal_id = renderers.NotAvailableValue() + display_name = renderers.NotAvailableValue() + + try: + dynamic_info = cls.parse_dynamic_info_value(values["DynamicInfo"]) + except KeyError: + vollog.debug("DynamicInfo value not found") + dynamic_info = None + + vollog.debug(dynamic_info) + + creation_time = dynamic_info.creation_time if dynamic_info is not None else None + last_run_time = dynamic_info.last_run_time if dynamic_info is not None else None + last_successful_run_time = ( + dynamic_info.last_successful_run_time if dynamic_info is not None else None + ) + + all_triggers = ( + trigger_set.triggers or [None] if trigger_set is not None else [None] + ) + + all_actions = action_set.actions or [None] if action_set is not None else [None] + + for action, trigger in itertools.product(all_actions, all_triggers): + + if action is not None: + if action.action_type in ( + ActionType.Exe, + ActionType.ComHandler, + ): + if action.action_args is None: + args = renderers.NotAvailableValue() + else: + args = action.action_args + else: + args = renderers.NotApplicableValue() + + if action.action_type == ActionType.Exe: + working_directory = ( + action.working_directory or renderers.NotAvailableValue() + ) + else: + working_directory = renderers.NotApplicableValue() + + else: + args = renderers.NotAvailableValue() + working_directory = renderers.NotAvailableValue() + + if trigger is not None and trigger.enabled is not None: + enabled = trigger.enabled + else: + enabled = renderers.NotAvailableValue() + + yield _ScheduledTaskEntry( + task_name, + principal_id, + display_name, + enabled, + creation_time or renderers.NotAvailableValue(), + last_run_time or renderers.NotAvailableValue(), + last_successful_run_time or renderers.NotAvailableValue(), + ( + trigger.trigger_type.name + if trigger is not None + else renderers.NotAvailableValue() + ), + ( + trigger.description or renderers.NotAvailableValue() + if trigger is not None + else renderers.NotAvailableValue() + ), + ( + action.action_type.name + if action is not None + else renderers.NotAvailableValue() + ), + ( + action.action + if action is not None + else renderers.NotAvailableValue() + ), + args, + ( + action_set.context + if action_set is not None + else renderers.NotAvailableValue() + ), + working_directory, + str(key.get_name()), + ) + + def _generator(self) -> Iterator[Tuple[int, _ScheduledTaskEntry]]: + kernel = self.context.modules[self.config["kernel"]] + + # Building the dictionary ahead of time is much better for performance + # vs looking up each service's DLL individually. + software_hive = self.get_software_hive(self.context, self.config_path, kernel) + if software_hive is None: + vollog.warning("Failed to get SOFTWARE hive") + return + + task_key_root, task_tree = self._get_task_keys(software_hive) + if task_key_root is None: + vollog.warning("Failed to get 'Tasks' key") + return + + if task_tree is not None: + task_name_map = _build_guid_name_map(task_tree) + else: + vollog.info("'Tree' key not found, can't map GUIDs to task names") + task_name_map = {} + + for key in task_key_root.get_subkeys(): + for task in self._parse_task_key(key, task_name_map): + yield 0, task + + def run(self): + return renderers.TreeGrid( + [ + ("Task Name", str), + ("Principal ID", str), + ("Display Name", str), + ("Enabled", bool), + ("Creation Time", datetime.datetime), + ("Last Run Time", datetime.datetime), + ("Last Successful Run Time", datetime.datetime), + ("Trigger Type", str), + ("Trigger Description", str), + ("Action Type", str), + ("Action", str), + ("Action Arguments", str), + ("Action Context", str), + ("Working Directory", str), + ("Key Name", str), + ], + ( + (indent, dataclasses.astuple(entry)) + for indent, entry in self._generator() + ), + ) + + +class TestActionsDecoding(unittest.TestCase): + def test_decode_exe_action(self): + # fmt: off + buf = struct.pack( + "512B", + *[ + 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4c, 0x00, + 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, + 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x6d, 0x00, 0x66, 0x66, 0x00, 0x00, + 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00, + 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, 0x5c, 0x00, + 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, 0x32, 0x00, + 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, + 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, 0x65, 0x00, + 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, 0x76, 0x00, + 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x5c, 0x00, + 0x70, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x73, 0x00, 0x68, 0x00, 0x65, 0x00, + 0x6c, 0x00, 0x6c, 0x00, 0x2e, 0x00, 0x65, 0x00, + 0x78, 0x00, 0x65, 0x00, 0x62, 0x01, 0x00, 0x00, + 0x2d, 0x00, 0x45, 0x00, 0x78, 0x00, 0x65, 0x00, + 0x63, 0x00, 0x75, 0x00, 0x74, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x50, 0x00, 0x6f, 0x00, + 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x79, 0x00, + 0x20, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, 0x00, + 0x69, 0x00, 0x63, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x64, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x49, 0x00, 0x6e, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, + 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, + 0x6f, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00, + 0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x53, 0x00, 0x74, 0x00, 0x79, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x48, 0x00, 0x69, 0x00, + 0x64, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6e, 0x00, + 0x20, 0x00, 0x22, 0x00, 0x26, 0x00, 0x20, 0x00, + 0x25, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, + 0x5c, 0x00, 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, + 0x32, 0x00, 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x73, 0x00, 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, + 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, + 0x76, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, + 0x5c, 0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, + 0x75, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x5c, 0x00, 0x53, 0x00, 0x6d, 0x00, 0x62, 0x00, + 0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x5c, 0x00, 0x44, 0x00, 0x69, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x75, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x64, 0x00, 0x53, 0x00, + 0x6d, 0x00, 0x62, 0x00, 0x31, 0x00, 0x2e, 0x00, + 0x70, 0x00, 0x73, 0x00, 0x31, 0x00, 0x20, 0x00, + 0x2d, 0x00, 0x53, 0x00, 0x63, 0x00, 0x65, 0x00, + 0x6e, 0x00, 0x61, 0x00, 0x72, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6c, 0x00, + 0x69, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + ) + + try: + actions = ActionSet.decode(buf) # type: ignore + self.assertEqual(len(actions), 1) + self.assertEqual(actions[0].action_type, ActionType.Exe) + except Exception: + self.fail( + "ActionDecoder.decode should not raise exception:\n%s" + % traceback.format_exc() + ) + + +class TestTriggersDecoding(unittest.TestCase): + def test_decode_all_triggers(self): + """ + Tests decoding a set of all triggers that can be constructed via the + Task Scheduler GUI interface. Ensures that the correct number of bytes + is being consumed for each trigger structure. + """ + buf = struct.pack( + "1808B", + # fmt: off + *[ + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, + 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, + 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, + 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, + 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, + 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, + 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, + 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, + 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, + 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, + 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, + 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, + 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, + 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, + 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, + 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, + 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, + 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, + 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, + 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, + 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, + 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, + 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, + 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, + 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ] + # fmt: on + ) + triggers = TriggerSet.decode(buf) + self.assertIsNotNone(triggers) + + def test_decode_triggers(self): + # fmt: off + buf = struct.pack( + "320B", + *[ + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x08, 0xA1, 0x40, 0x42, 0x48, 0x48, 0x48, 0x48, + 0x7A, 0x7F, 0x59, 0xDC, 0x48, 0x48, 0x48, 0x48, + 0x22, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x49, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x63, 0x00, 0x74, 0x00, + 0x69, 0x00, 0x76, 0x00, 0x65, 0x00, 0x55, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, + 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x05, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x0C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x04, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x2C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x80, 0x51, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x2C, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xD9, 0x04, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + ] + ) + # fmt: on + triggers = TriggerSet.decode(buf) + self.assertIsNotNone(triggers) + if not triggers: + return + self.assertGreater(len(triggers.triggers), 0) diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index c8ddc19fdd..e48684b318 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -28,6 +28,27 @@ def wintime_to_datetime( return renderers.UnparsableValue() +def windows_bytes_to_guid(buf: bytes) -> str: + """ + Converts 16 raw bytes to a windows GUID. + + Raises ValueError if the provided buffer is not exactly 16 bytes. + """ + if len(buf) != 16: + raise ValueError("Expected 16 bytes for GUID") + + head_components = [format(v, "x") for v in struct.unpack("HQ", + buf[8:10] + b"\x00\x00" + buf[10:16], + ) + ] + combined = head_components + tail_component + return "{" + "-".join(combined) + "}" + + def unixtime_to_datetime( unixtime: int, ) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: From fb4dd924ceaffa04fd39dd3a86009f110748cd85 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 11 Oct 2024 10:15:43 -0500 Subject: [PATCH 188/250] Linux: Netfilter - fix traceback on missing module name When the module name can't be found, `None` ends up in this field, causing a traceback when rendering. This fixes it by replacing `None` with `renderers.NotAvailableValue()`. This occurred in the context of a netfilter hook from a hidden module. --- volatility3/framework/plugins/linux/netfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index d392a83710..f576c073c1 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -714,7 +714,7 @@ def _format_fields(self, fields): hook_name, priority, format_hints.Hex(hook_func), - module_name, + module_name or renderers.NotAvailableValue(), str(hooked), ) From e3521147d6c922a7e25d4481edcd6d4016888f74 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sun, 13 Oct 2024 09:26:37 +1100 Subject: [PATCH 189/250] Linux - Add support for task threads in kernels >= 6.7 --- .../symbols/linux/extensions/__init__.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index aa18b8262a..0dab0db433 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -359,20 +359,29 @@ def is_user_thread(self) -> bool: """ return not self.is_kernel_thread and self.tgid != self.pid - def get_threads(self) -> Iterable[interfaces.objects.ObjectInterface]: - """Returns a list of the task_struct based on the list_head - thread_node structure.""" + def _get_tasks_iterable(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns the respective iterable to obtain the threads in this process""" + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + task_struct_symname = f"{vmlinux.symbol_table_name}{constants.BANG}task_struct" + if vmlinux.get_type("task_struct").has_member("signal") and vmlinux.get_type( + "signal_struct" + ).has_member("thread_head"): + # kernels >= 6.7 - via signals + return self.signal.thread_head.to_list(task_struct_symname, "thread_node") + elif vmlinux.get_type("task_struct").has_member("thread_group"): + # kernels < 6.7 - via thread_group + return self.thread_group.to_list(task_struct_symname, "thread_group") - task_symbol_table_name = self.get_symbol_table_name() + raise AttributeError("Unable to find the root dentry") - # iterating through the thread_list from thread_group - # this allows iterating through pointers to grab the - # threads and using the thread_group offset to get the - # corresponding task_struct - for task in self.thread_group.to_list( - f"{task_symbol_table_name}{constants.BANG}task_struct", "thread_group" - ): - yield task + def get_threads(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns each thread in this process""" + tasks_iterable = self._get_tasks_iterable() + threads_seen = set([self.vol.offset]) + for task in tasks_iterable: + if task.vol.offset not in threads_seen: + threads_seen.add(task.vol.offset) + yield task class fs_struct(objects.StructType): From 15a51cf1ec1e139193097976c115c820861bc034 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 14 Oct 2024 16:13:56 +1100 Subject: [PATCH 190/250] Linux - Add hlist_head object extension. Fix #1313 --- .../framework/symbols/linux/__init__.py | 1 + .../symbols/linux/extensions/__init__.py | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 219f120ee7..1b7a331f87 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -22,6 +22,7 @@ def __init__(self, *args, **kwargs) -> None: # Set-up Linux specific types self.set_type_class("file", extensions.struct_file) self.set_type_class("list_head", extensions.list_head) + self.set_type_class("hlist_head", extensions.hlist_head) self.set_type_class("mm_struct", extensions.mm_struct) self.set_type_class("super_block", extensions.super_block) self.set_type_class("task_struct", extensions.task_struct) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index aa18b8262a..4b1df2a0ba 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -844,7 +844,7 @@ def get_subdirs(self) -> interfaces.objects.ObjectInterface: if self.has_member("d_sib") and self.has_member("d_children"): # kernels >= 6.8 walk_member = "d_sib" - list_head_member = self.d_children.first + list_head_member = self.d_children elif self.has_member("d_child") and self.has_member("d_subdirs"): # 2.5.0 <= kernels < 6.8 walk_member = "d_child" @@ -961,6 +961,43 @@ def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) +class hlist_head(objects.StructType, collections.abc.Iterable): + def to_list( + self, + symbol_type: str, + member: str, + ) -> Iterator[interfaces.objects.ObjectInterface]: + """Returns an iterator of the entries in the list. + + This is a doubly linked list; however, it is not circular, so the 'forward' field + doesn't make sense. Also, the sentinel concept doesn't make sense here either; + unlike list_head, the head and nodes each have their own distinct types. A list_head + cannot be a node by itself. + - The 'pprev' of the first 'hlist_node' points to the 'hlist_head', not to the last node. + - The last element 'next' member is NULL + + Args: + symbol_type: Type of the list elements + member: Name of the list_head member in the list elements + + Yields: + Objects of the type specified via the "symbol_type" argument. + + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + + current = self.first + while current and current.is_readable(): + yield linux.LinuxUtilities.container_of( + current, symbol_type, member, vmlinux + ) + + current = current.next + + def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: + return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) + + class files_struct(objects.StructType): def get_fds(self) -> interfaces.objects.ObjectInterface: if self.has_member("fdt"): From f00c4c3c392a26bea29ba87d961c3a8f51092829 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Mon, 14 Oct 2024 16:35:43 +1100 Subject: [PATCH 191/250] Allows to create objects when using the same symbol table --- volatility3/framework/contexts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 4c169728cc..6961d93282 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -245,7 +245,7 @@ def object( """ if constants.BANG not in object_type: object_type = self.symbol_table_name + constants.BANG + object_type - else: + elif not object_type.startswith(self.symbol_table_name + constants.BANG): raise ValueError( "Cannot reference another module when constructing an object" ) From e739d96a33217e013f0b126ab2c11007cf059cee Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 14 Oct 2024 14:03:38 +0200 Subject: [PATCH 192/250] dwarf2json rust type confusion sanity check --- volatility3/schemas/__init__.py | 54 +++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index be120f2afa..3ca00e5dc6 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -6,8 +6,8 @@ import json import logging import os -from typing import Any, Dict, Optional, Set - +import re +from typing import Any, Dict, Optional, Set, Tuple from volatility3.framework import constants vollog = logging.getLogger(__name__) @@ -77,6 +77,17 @@ def valid( input: Dict[str, Any], schema: Dict[str, Any], use_cache: bool = True ) -> bool: """Validates a json schema.""" + producer = input.get("metadata", {}).get("producer", {}) + if producer and producer.get("name") == "dwarf2json": + dwarf2json_version = parse_producer_version(producer.get("version", "")) + # No warnings if version couldn't be parsed, as it's not our role here + # to validate the schema. + if dwarf2json_version: + if dwarf2json_check_rust_type_confusion(input, dwarf2json_version): + vollog.warning( + "This ISF was generated by dwarf2json < 0.9.0, which is known to produce inaccurate results (see dwarf2json GitHub issue #63)." + ) + input_hash = create_json_hash(input, schema) if input_hash in cached_validations and use_cache: return True @@ -98,3 +109,42 @@ def valid( record_cached_validations(cached_validations) return True + + +def parse_producer_version(version_string: str) -> Optional[Tuple[int]]: + """Parses a producer version and returns a tuple of identifiers. + + Args: + version_string: string containing dot-separated integers, + expected to follow the Volatility3 versioning schema + + Returns: + A tuple containing each version identifier + """ + identifiers = re.search("^(\\d+)[.](\\d+)[.](\\d+)$", version_string) + if not identifiers: + return None + + return tuple(int(d) for d in identifiers.groups()) + + +# dwarf2json sanity checks # +def dwarf2json_check_rust_type_confusion( + input: Dict[str, Any], dwarf2json_version: Tuple[int] +) -> bool: + """dwarf2json sanity check for Rust and C types confusion: + - dwarf2json #63 + - volatility3 #1305 + + Args: + dwarf2json_version: a tuple containing each version identifier + + Returns: + True if the issue was detected + """ + + return "rust_helper_BUG" in input.get("symbols", {}) and dwarf2json_version < ( + 0, + 9, + 0, + ) From d98c7ebb1d21f3074d413e7097bde7448932db85 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 16 Oct 2024 15:05:02 +1100 Subject: [PATCH 193/250] linux: hidden_modules: Make the fast method the default. Remove vol2 and fall back to a 1-byte alignment scan if addresses aren't aligned to the L1 cache size --- .../framework/plugins/linux/hidden_modules.py | 216 +++--------------- 1 file changed, 26 insertions(+), 190 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 6542f58f3a..69b4492610 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -32,12 +32,6 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) ), - requirements.BooleanRequirement( - name="fast", - description="Fast scan method. Recommended only for kernels 4.2 and above", - optional=True, - default=False, - ), ] @staticmethod @@ -86,134 +80,13 @@ def get_modules_memory_boundaries( return modules_addr_min, modules_addr_max - @staticmethod - def _get_module_state_values_bytes( - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> List[bytes]: - """Retrieve the module state values bytes by introspecting its enum type - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - A list with the module state values bytes - """ - vmlinux = context.modules[vmlinux_module_name] - module_state_type_template = vmlinux.get_type("module").vol.members["state"][1] - data_format = module_state_type_template.base_type.vol.data_format - values = module_state_type_template.choices.values() - values_bytes = [ - objects.convert_value_to_data(value, int, data_format) - for value in sorted(values) - ] - return values_bytes - - @classmethod - def _get_hidden_modules_vol2( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - known_module_addresses: Set[int], - modules_memory_boundaries: Tuple, - ) -> Iterable[interfaces.objects.ObjectInterface]: - """Enumerate hidden modules using the traditional implementation. - - This is a port of the Volatility2 plugin, with minor code improvements. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - known_module_addresses: Set with known module addresses - modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - - Yields: - module objects - """ - vmlinux = context.modules[vmlinux_module_name] - vmlinux_layer = context.layers[vmlinux.layer_name] - - check_nums = ( - 3000, - 2800, - 2700, - 2500, - 2300, - 2100, - 2000, - 1500, - 1300, - 1200, - 1024, - 512, - 256, - 128, - 96, - 64, - 48, - 32, - 24, - ) - modules_addr_min, modules_addr_max = modules_memory_boundaries - modules_addr_min = modules_addr_min & ~0xFFF - modules_addr_max = (modules_addr_max & ~0xFFF) + vmlinux_layer.page_size - - check_bufs = [] - replace_bufs = [] - minus_size = vmlinux.get_type("pointer").size - null_pointer_bytes = b"\x00" * minus_size - for num in check_nums: - check_bufs.append(b"\x00" * num) - replace_bufs.append((b"\xff" * (num - minus_size)) + null_pointer_bytes) - - all_ffs = b"\xff" * 4096 - scan_list = [] - for page_addr in range( - modules_addr_min, modules_addr_max, vmlinux_layer.page_size - ): - content_fixed = all_ffs - with contextlib.suppress( - exceptions.InvalidAddressException, - exceptions.PagedInvalidAddressException, - ): - content = vmlinux_layer.read(page_addr, vmlinux_layer.page_size) - - all_nulls = all(x == 0 for x in content) - if content and not all_nulls: - content_fixed = content - for check_bytes, replace_bytes in zip(check_bufs, replace_bufs): - content_fixed = content_fixed.replace( - check_bytes, replace_bytes - ) - - scan_list.append(content_fixed) - - scan_buf = b"".join(scan_list) - del scan_list - - module_state_values_bytes = cls._get_module_state_values_bytes( - context, vmlinux_module_name - ) - values_bytes_pattern = b"|".join(module_state_values_bytes) - # f'strings cannot be combined with bytes literals - for cur_addr in re.finditer(b"(?=(%s))" % values_bytes_pattern, scan_buf): - module_addr = modules_addr_min + cur_addr.start() - - if module_addr in known_module_addresses: - continue - - module = vmlinux.object("module", offset=module_addr, absolute=True) - if module and module.is_valid(): - yield module - @classmethod def _get_module_address_alignment( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, ) -> int: - """Obtain the module memory address alignment. This is only used with the fast scan method. + """Obtain the module memory address alignment. struct module is aligned to the L1 cache line, which is typically 64 bytes for most common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this @@ -231,8 +104,24 @@ def _get_module_address_alignment( # essential for retrieving type metadata in the future. return 64 + @staticmethod + def _validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation + + Returns: + True if all the addresses meet the alignment + """ + return all(addr % address_alignment == 0 for addr in addresses) + @classmethod - def _get_hidden_modules_fast( + def get_hidden_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str, @@ -268,6 +157,14 @@ def _get_hidden_modules_fast( module_address_alignment = cls._get_module_address_alignment( context, vmlinux_module_name ) + if not cls._validate_alignment_patterns( + known_module_addresses, module_address_alignment + ): + vollog.warning( + f"Module addresses aren't aligned to {module_address_alignment} bytes. " + "Switching to 1 byte aligment scan method." + ) + module_address_alignment = 1 mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") @@ -302,66 +199,6 @@ def _get_hidden_modules_fast( if module and module.is_valid(): yield module - @staticmethod - def _validate_alignment_patterns( - addresses: Iterable[int], - address_alignment: int, - ) -> bool: - """Check if the memory addresses meet our alignments patterns - - Args: - addresses: Iterable with the address values - address_alignment: Number of bytes for alignment validation - - Returns: - True if all the addresses meet the alignment - """ - return all(addr % address_alignment == 0 for addr in addresses) - - @classmethod - def get_hidden_modules( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - known_module_addresses: Set[int], - modules_memory_boundaries: Tuple, - fast_method: bool = False, - ) -> Iterable[interfaces.objects.ObjectInterface]: - """Enumerate hidden modules - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - known_module_addresses: Set with known module addresses - modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - fast_method: If True, it uses the fast method. Otherwise, it uses the traditional one. - Yields: - module objects - """ - if fast_method: - module_address_alignment = cls._get_module_address_alignment( - context, vmlinux_module_name - ) - if cls._validate_alignment_patterns( - known_module_addresses, module_address_alignment - ): - scan_method = cls._get_hidden_modules_fast - else: - vollog.warning( - f"Module addresses aren't aligned to {module_address_alignment} bytes. " - "Switching to the traditional scan method." - ) - scan_method = cls._get_hidden_modules_vol2 - else: - scan_method = cls._get_hidden_modules_vol2 - - yield from scan_method( - context, - vmlinux_module_name, - known_module_addresses, - modules_memory_boundaries, - ) - @classmethod def get_lsmod_module_addresses( cls, @@ -399,7 +236,6 @@ def _generator(self): vmlinux_module_name, known_module_addresses, modules_memory_boundaries, - fast_method=self.config.get("fast"), ): module_addr = module.vol.offset module_name = module.get_name() or renderers.NotAvailableValue() From 0ddd9210a8e95a9172a36f4939be68fff6edd8ed Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 16 Oct 2024 15:08:59 +1100 Subject: [PATCH 194/250] linux: hidden_modules: Remove unused module imports --- volatility3/framework/plugins/linux/hidden_modules.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 69b4492610..679f2b00a3 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -1,9 +1,7 @@ # This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import re import logging -import contextlib from typing import List, Set, Tuple, Iterable from volatility3.framework import renderers, interfaces, exceptions, objects from volatility3.framework.constants.architectures import LINUX_ARCHS From 18f7f035ee83080d2ea660acfef05453c8e04ab5 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 18 Oct 2024 12:29:09 +1100 Subject: [PATCH 195/250] linux: fix datetime import and remove unused ones --- .../framework/symbols/linux/extensions/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0dab0db433..d9392b9ba3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -7,13 +7,12 @@ import functools import binascii import stat -from datetime import datetime +import datetime import socket as socket_module from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion -from volatility3.framework.configuration import requirements from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS @@ -1882,7 +1881,7 @@ def get_capabilities(self) -> int: class timespec64(objects.StructType): - def to_datetime(self) -> datetime: + def to_datetime(self) -> datetime.datetime: """Returns the respective aware datetime""" dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) @@ -1958,7 +1957,7 @@ def get_inode_type(self) -> Union[str, None]: else: return None - def _time_member_to_datetime(self, member) -> datetime: + def _time_member_to_datetime(self, member) -> datetime.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 @@ -1977,7 +1976,7 @@ def _time_member_to_datetime(self, member) -> datetime: "Unsupported kernel inode type implementation" ) - def get_access_time(self) -> datetime: + def get_access_time(self) -> datetime.datetime: """Returns the inode's last access time This is updated when inode contents are read @@ -1986,7 +1985,7 @@ def get_access_time(self) -> datetime: """ return self._time_member_to_datetime("i_atime") - def get_modification_time(self) -> datetime: + def get_modification_time(self) -> datetime.datetime: """Returns the inode's last modification time This is updated when the inode contents change @@ -1996,7 +1995,7 @@ def get_modification_time(self) -> datetime: return self._time_member_to_datetime("i_mtime") - def get_change_time(self) -> datetime: + def get_change_time(self) -> datetime.datetime: """Returns the inode's last change time This is updated when the inode metadata changes From f8224684078dd9937da4b0924f504a0bfb25b8bf Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 18 Oct 2024 12:40:02 +1100 Subject: [PATCH 196/250] linux: Implement boot time support in the Volatility 3 core framework --- .../framework/constants/linux/__init__.py | 3 + .../framework/symbols/linux/__init__.py | 98 +++++++++ .../symbols/linux/extensions/__init__.py | 197 ++++++++++++++++++ 3 files changed, 298 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 3eabc2341a..a481e1ba96 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -302,3 +302,6 @@ class ELF_CLASS(IntEnum): ELFCLASSNONE = 0 ELFCLASS32 = 1 ELFCLASS64 = 2 + + +NSEC_PER_SEC = 1e9 diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 219f120ee7..95b4762634 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -3,11 +3,15 @@ # import math import contextlib +import datetime +import dataclasses from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects +from volatility3.framework.renderers import conversion +from volatility3.framework.constants.linux import NSEC_PER_SEC from volatility3.framework.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions @@ -830,3 +834,97 @@ def get_cached_pages(self) -> interfaces.objects.ObjectInterface: page = self.vmlinux.object("page", offset=page_addr, absolute=True) if page: yield page + + +@dataclasses.dataclass +class TimespecVol3(object): + """Internal helper class to handle all required timespec operations, convertions and + adjustments. + + NOTE: This is intended for exclusive use with get_boottime() and its related functions. + """ + + tv_sec: int = 0 + tv_nsec: int = 0 + + @classmethod + def new_from_timespec(cls, timespec) -> "TimespecVol3": + """Creates a new instance from a TimespecVol3 or timespec64 object""" + if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): + raise TypeError("It requires either a TimespecVol3 or timespec64 type") + + tv_sec = int(timespec.tv_sec) + tv_nsec = int(timespec.tv_nsec) + return cls(tv_sec=tv_sec, tv_nsec=tv_nsec) + + @classmethod + def new_from_nsec(cls, nsec) -> "TimespecVol3": + """Creates a new instance from an integer in nanoseconds""" + + # Based on ns_to_timespec64() + if nsec > 0: + tv_sec = nsec // NSEC_PER_SEC + tv_nsec = nsec % NSEC_PER_SEC + elif nsec < 0: + tv_sec = -((-nsec - 1) // NSEC_PER_SEC) - 1 + rem = (-nsec - 1) % NSEC_PER_SEC + tv_nsec = NSEC_PER_SEC - rem - 1 + else: + tv_sec = tv_nsec = 0 + + return cls(tv_sec=tv_sec, tv_nsec=tv_nsec) + + def to_datetime(self) -> datetime.datetime: + """Converts this TimespecVol3 to a UTC aware datetime""" + return conversion.unixtime_to_datetime( + self.tv_sec + self.tv_nsec / NSEC_PER_SEC + ) + + def to_timedelta(self) -> datetime.timedelta: + """Converts this TimespecVol3 to timedelta""" + return datetime.timedelta(seconds=self.tv_sec + self.tv_nsec / NSEC_PER_SEC) + + def __add__(self, timespec) -> "TimespecVol3": + """Returns a new TimespecVol3 object that sums the current values with those + in the timespec argument""" + if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): + raise TypeError("Cannot add a TimespecVol3 to this object") + + result = TimespecVol3( + tv_sec=self.tv_sec + timespec.tv_sec, + tv_nsec=self.tv_nsec + timespec.tv_nsec, + ) + + result.normalize() + + return result + + def __sub__(self, timespec) -> "TimespecVol3": + """Returns a new TimespecVol3 object that subtracts the values in the timespec + argument from the current object's values""" + if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): + raise TypeError("Cannot add a TimespecVol3 to this object") + + result = TimespecVol3( + tv_sec=self.tv_sec - timespec.tv_sec, + tv_nsec=self.tv_nsec - timespec.tv_nsec, + ) + result.normalize() + + return result + + def normalize(self): + """Normalize any overflow in tv_sec and tv_nsec after previous addition or subtractions""" + # Based on kernel's set_normalized_timespec64() + while self.tv_nsec >= NSEC_PER_SEC: + self.tv_nsec -= NSEC_PER_SEC + self.tv_sec += 1 + + while self.tv_nsec < 0: + self.tv_nsec += NSEC_PER_SEC + self.tv_sec -= 1 + + def negate(self): + """Negates the sign of both tv_sec and tv_nsec""" + self.tv_sec = -self.tv_sec + self.tv_nsec = -self.tv_nsec diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d9392b9ba3..b3e8361025 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -382,6 +382,203 @@ def get_threads(self) -> Iterable[interfaces.objects.ObjectInterface]: threads_seen.add(task.vol.offset) yield task + def _get_task_start_time(self) -> datetime.timedelta: + """Returns the task's monotonic start_time as a timedelta. + + Returns: + The task's start time as a timedelta object. + """ + for member_name in ("start_boottime", "real_start_time", "start_time"): + if self.has_member(member_name): + start_time_obj = self.member(member_name) + start_time_obj_type = start_time_obj.vol.type_name + start_time_obj_type_name = start_time_obj_type.split(constants.BANG)[1] + if start_time_obj_type_name != "timespec": + # kernels >= 3.17 real_start_time and start_time are u64 + # kernels >= 5.5 uses start_boottime which is also a u64 + start_time = linux.TimespecVol3.new_from_nsec(start_time_obj) + else: + # kernels < 3.17 real_start_time and start_time are timespec + start_time = linux.TimespecVol3.new_from_timespec(start_time_obj) + + # This is relative to the boot time so it makes sense to be a timedelta. + return start_time.to_timedelta() + + raise AttributeError("Unsupported task_struct start_time member") + + def get_time_namespace(self) -> Optional[interfaces.objects.ObjectInterface]: + """Returns the task's time namespace""" + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + if not self.has_member("nsproxy"): + # kernels < 2.6.19: ab516013ad9ca47f1d3a936fa81303bfbf734d52 + return None + + if not vmlinux.get_type("nsproxy").has_member("time_ns"): + # kernels < 5.6 769071ac9f20b6a447410c7eaa55d1a5233ef40c + return None + + return self.nsproxy.time_ns + + def get_time_namespace_id(self) -> int: + """Returns the task's time namespace ID.""" + time_ns = self.get_time_namespace() + if not time_ns: + # kernels < 5.6 + return + + # We are good. ns_common (ns) was introduced in kernels 3.19. So by the time the + # time namespace was added in kernels 5.6, it already included the ns member. + return time_ns.ns.inum + + def _get_time_namespace_offsets( + self, + ) -> Optional[interfaces.objects.ObjectInterface]: + """Returns the time offsets from the task's time namespace.""" + time_ns = self.get_time_namespace() + if not time_ns: + # kernels < 5.6 + return + + if not time_ns.has_member("offsets"): + # kernels < 5.6 af993f58d69ee9c1f421dfc87c3ed231c113989c + return None + + return time_ns.offsets + + def get_time_namespace_monotonic_offset( + self, + ) -> Optional[interfaces.objects.ObjectInterface]: + """Gets task's time namespace monotonic offset + + Returns: + a kernel's timespec64 object with the monotonic offset + """ + time_namespace_offsets = self._get_time_namespace_offsets() + if not time_namespace_offsets: + return None + + return time_namespace_offsets.monotonic + + def _get_time_namespace_boottime_offset( + self, + ) -> Optional[interfaces.objects.ObjectInterface]: + """Gets task's time namespace boottime offset + + Returns: + a kernel's timespec64 object with the boottime offset + """ + time_namespace_offsets = self._get_time_namespace_offsets() + if not time_namespace_offsets: + return None + + return time_namespace_offsets.boottime + + def _get_boottime_raw(self) -> "linux.TimespecVol3": + """Returns the boot time in a TimespecVol3.""" + + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + if vmlinux.has_symbol("tk_core"): + # kernels >= 3.17 | tk_core | 3fdb14fd1df70325e1e91e1203a699a4803ed741 + tk_core = vmlinux.object_from_symbol("tk_core") + timekeeper = tk_core.timekeeper + if not timekeeper.offs_real.has_member("tv64"): + # kernels >= 4.10 - Tested on Ubuntu 6.8.0-41 + boottime_nsec = timekeeper.offs_real - timekeeper.offs_boot + else: + # 3.17 <= kernels < 4.10 - Tested on Ubuntu 4.4.0-142 + boottime_nsec = timekeeper.offs_real.tv64 - timekeeper.offs_boot.tv64 + return linux.TimespecVol3.new_from_nsec(boottime_nsec) + + elif vmlinux.has_symbol("timekeeper") and vmlinux.get_type( + "timekeeper" + ).has_member("wall_to_monotonic"): + # 3.4 <= kernels < 3.17 - Tested on Ubuntu 3.13.0-185 + timekeeper = vmlinux.object_from_symbol("timekeeper") + + # timekeeper.wall_to_monotonic is timespec + boottime = linux.TimespecVol3.new_from_timespec( + timekeeper.wall_to_monotonic + ) + + boottime += timekeeper.total_sleep_time + + boottime.negate() + boottime.normalize() + + return boottime + + elif vmlinux.has_symbol("wall_to_monotonic"): + # kernels < 3.4 - Tested on Debian7 3.2.0-4 (3.2.57-3+deb7u2) + wall_to_monotonic = vmlinux.object_from_symbol("wall_to_monotonic") + boottime = linux.TimespecVol3.new_from_timespec(wall_to_monotonic) + if vmlinux.has_symbol("total_sleep_time"): + # 2.6.23 <= kernels < 3.4 7c3f1a573237b90ef331267260358a0ec4ac9079 + total_sleep_time = vmlinux.object_from_symbol("total_sleep_time") + full_type_name = total_sleep_time.vol.type_name + type_name = full_type_name.split(constants.BANG)[1] + if type_name == "timespec": + # kernels >= 2.6.32 total_sleep_time is a timespec + boottime += total_sleep_time + else: + # kernels < 2.6.32 total_sleep_time is an unsigned long as seconds + boottime.tv_sec += total_sleep_time + + boottime.negate() + boottime.normalize() + + return boottime + + raise exceptions.VolatilityException("Unsupported") + + def get_boottime(self, root_time_namespace: bool = True) -> datetime.datetime: + """Returns the boot time in UTC as a datetime. + + Args: + root_time_namespace: If True, it returns the boot time as seen from the root + time namespace. Otherwise, it returns the boot time relative to the + task's time namespace. + + Returns: + A datetime with the UTC boot time. + """ + boottime = self._get_boottime_raw() + if not boottime: + return None + + if not root_time_namespace: + # Shift boot timestamp according to the task's time namespace offset + boottime_offset_timespec = self._get_time_namespace_boottime_offset() + if boottime_offset_timespec: + # Time namespace support is from kernels 5.6 + boottime -= boottime_offset_timespec + + return boottime.to_datetime() + + def get_create_time(self) -> datetime.datetime: + """Retrieves the task's start time from its time namespace. + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + task: A reference task + + Returns: + A datetime with task's start time + """ + # Typically, we want to see the creation time seen from the root time namespace + boottime = self.get_boottime(root_time_namespace=True) + + # The kernel exports only tv_sec to procfs, see kernel's show_stat(). + # This means user-space tools, like those in the procps package (e.g., ps, top, etc.), + # only use the boot time seconds to compute dates relatives to this. + boottime = boottime.replace(microsecond=0) + + task_start_time_timedelta = self._get_task_start_time() + + # NOTE: Do NOT apply the task's time namespace offsets here. While the kernel uses + # timens_add_boottime_ns(), it's not needed here since we're seeing it from the + # root time namespace, not within the task's own time namespace + return boottime + task_start_time_timedelta + class fs_struct(objects.StructType): def get_root_dentry(self): From 69512dc9911fc6ce8d0e1ab65926663341513974 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 18 Oct 2024 12:58:02 +1100 Subject: [PATCH 197/250] Linux: pslist: Add creation time column and timeline support to the linux.pslist plugin --- volatility3/framework/plugins/linux/pslist.py | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 1888bd7b83..b05d69c7ac 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -1,6 +1,7 @@ # This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import datetime from typing import Any, Callable, Iterable, List, Tuple from volatility3.framework import interfaces, renderers @@ -9,15 +10,16 @@ from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux.extensions import elf +from volatility3.plugins import timeliner from volatility3.plugins.linux import elfs -class PsList(interfaces.plugins.PluginInterface): +class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the processes present in a particular linux memory image.""" _required_framework_version = (2, 0, 0) - _version = (2, 2, 1) + _version = (2, 3, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -81,7 +83,7 @@ def filter_func(x): @classmethod def get_task_fields( cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False - ) -> Tuple[int, int, int, str]: + ) -> Tuple[int, int, int, int, str, datetime.datetime]: """Extract the fields needed for the final output Args: @@ -96,13 +98,14 @@ def get_task_fields( tid = task.pid ppid = task.parent.tgid if task.parent else 0 name = utility.array_to_string(task.comm) + start_time = task.get_create_time() if decorate_comm: if task.is_kernel_thread: name = f"[{name}]" elif task.is_user_thread: name = f"{{{name}}}" - task_fields = (task.vol.offset, pid, tid, ppid, name) + task_fields = (task.vol.offset, pid, tid, ppid, name, start_time) return task_fields def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str: @@ -177,7 +180,9 @@ def _generator( else: file_output = "Disabled" - offset, pid, tid, ppid, name = self.get_task_fields(task, decorate_comm) + offset, pid, tid, ppid, name, creation_time = self.get_task_fields( + task, decorate_comm + ) yield 0, ( format_hints.Hex(offset), @@ -185,6 +190,7 @@ def _generator( tid, ppid, name, + creation_time or renderers.NotAvailableValue(), file_output, ) @@ -233,8 +239,23 @@ def run(self): ("TID", int), ("PPID", int), ("COMM", str), + ("CREATION TIME", datetime.datetime), ("File output", str), ] return renderers.TreeGrid( columns, self._generator(filter_func, include_threads, decorate_comm, dump) ) + + def generate_timeline(self): + pids = self.config.get("pid") + filter_func = self.create_pid_filter(pids) + for task in self.list_tasks( + self.context, self.config["kernel"], filter_func, include_threads=True + ): + offset, user_pid, user_tid, _user_ppid, name, creation_time = ( + self.get_task_fields(task) + ) + + description = f"Process {user_pid}/{user_tid} {name} ({offset})" + + yield (description, timeliner.TimeLinerType.CREATED, creation_time) From d25df2357acf157641a6b30a7a9a30d0261bf147 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 18 Oct 2024 12:59:23 +1100 Subject: [PATCH 198/250] Linux: pslist: Add the boottime plugin --- .../framework/plugins/linux/boottime.py | 98 +++++++++++++++++++ volatility3/framework/plugins/timeliner.py | 13 ++- 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 volatility3/framework/plugins/linux/boottime.py diff --git a/volatility3/framework/plugins/linux/boottime.py b/volatility3/framework/plugins/linux/boottime.py new file mode 100644 index 0000000000..eeee418f85 --- /dev/null +++ b/volatility3/framework/plugins/linux/boottime.py @@ -0,0 +1,98 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import datetime +from typing import List, Tuple, Iterable + + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.plugins import timeliner +from volatility3.plugins.linux import pslist + + +class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): + """Shows the time the system was started""" + + _required_framework_version = (2, 11, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 3, 0) + ), + ] + + @classmethod + def get_time_namespaces_bootime( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Iterable[Tuple[int, int, int, str, datetime.datetime]]: + """Enumerates tasks' boot times based on their time namespaces. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + pids: Pid list + unique: Filter unique time namespaces + + Yields: + A tuple with the fields to show in the plugin output. + """ + time_namespace_ids = set() + for task in pslist.PsList.list_tasks(context, vmlinux_module_name): + time_namespace_id = task.get_time_namespace_id() + # If it cannot get the time namespace i.e. kernels < 5.6, this still works + # using None to just get the first tasks + if time_namespace_id in time_namespace_ids: + continue + time_namespace_ids.add(time_namespace_id) + boottime = task.get_boottime(root_time_namespace=False) + + fields = ( + time_namespace_id, + boottime, + ) + yield fields + + def _generator(self): + for ( + time_namespace_id, + boottime, + ) in self.get_time_namespaces_bootime( + self.context, + self.config["kernel"], + ): + fields = [ + time_namespace_id or renderers.NotAvailableValue(), + boottime, + ] + yield 0, fields + + def generate_timeline(self): + for ( + time_namespace_id, + boottime, + ) in self.get_time_namespaces_bootime( + self.context, + self.config["kernel"], + ): + description = f"System boot time for time namespace {time_namespace_id}" + + yield description, timeliner.TimeLinerType.BOOTTIME, boottime + + def run(self): + columns = [ + ("TIME NS", int), + ("Boot Time", datetime.datetime), + ] + return renderers.TreeGrid(columns, self._generator()) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index c754e43eff..70da0c4fbc 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -23,6 +23,7 @@ class TimeLinerType(enum.IntEnum): MODIFIED = 2 ACCESSED = 3 CHANGED = 4 + BOOTTIME = 5 class TimeLinerInterface(metaclass=abc.ABCMeta): @@ -171,6 +172,10 @@ def _generator( TimeLinerType.CHANGED, renderers.NotApplicableValue(), ), + times.get( + TimeLinerType.BOOTTIME, + renderers.NotApplicableValue(), + ), ], ) ) @@ -178,11 +183,11 @@ def _generator( # Write each entry because the body file doesn't need to be sorted if fp: times = self.timeline[(plugin_name, item)] - # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime + # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime|boottime if self._any_time_present(times): fp.write( - "|{} - {}|0|0|0|0|0|{}|{}|{}|{}\n".format( + "|{} - {}|0|0|0|0|0|{}|{}|{}|{}|{}\n".format( plugin_name, self._sanitize_body_format(item), self._text_format( @@ -197,6 +202,9 @@ def _generator( self._text_format( times.get(TimeLinerType.CREATED, "0") ), + self._text_format( + times.get(TimeLinerType.BOOTTIME, "0") + ), ) ) except Exception as e: @@ -320,6 +328,7 @@ def run(self): ("Modified Date", datetime.datetime), ("Accessed Date", datetime.datetime), ("Changed Date", datetime.datetime), + ("Boot Date", datetime.datetime), ], generator=self._generator(plugins_to_run), ) From 1651ecb1d70bb32868be864c473ef4a346141381 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 18 Oct 2024 13:58:27 +1100 Subject: [PATCH 199/250] linux: boottime api: Fix explicit returns mixed with implicit (fall through) returns --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b3e8361025..2784aeda70 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -424,7 +424,7 @@ def get_time_namespace_id(self) -> int: time_ns = self.get_time_namespace() if not time_ns: # kernels < 5.6 - return + return None # We are good. ns_common (ns) was introduced in kernels 3.19. So by the time the # time namespace was added in kernels 5.6, it already included the ns member. @@ -437,7 +437,7 @@ def _get_time_namespace_offsets( time_ns = self.get_time_namespace() if not time_ns: # kernels < 5.6 - return + return None if not time_ns.has_member("offsets"): # kernels < 5.6 af993f58d69ee9c1f421dfc87c3ed231c113989c From 65daab6cfa986130a33d6c63faab00d5e500534a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 22 Oct 2024 21:47:13 +0100 Subject: [PATCH 200/250] Linux: Fix minor coding style transgression --- volatility3/framework/plugins/linux/ptrace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/ptrace.py b/volatility3/framework/plugins/linux/ptrace.py index e501f32948..e467ee6440 100644 --- a/volatility3/framework/plugins/linux/ptrace.py +++ b/volatility3/framework/plugins/linux/ptrace.py @@ -6,7 +6,7 @@ from typing import List, Iterator from volatility3.framework import renderers, interfaces -from volatility3.framework.constants.architectures import LINUX_ARCHS +from volatility3.framework.constants import architectures from volatility3.framework.objects import utility from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins @@ -27,7 +27,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=LINUX_ARCHS, + architectures=architectures.LINUX_ARCHS, ), requirements.PluginRequirement( name="pslist", plugin=pslist.PsList, version=(2, 2, 0) From db8667f88abec074f3165ecfd04219e7ef72fbd1 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 23 Oct 2024 08:55:44 +1100 Subject: [PATCH 201/250] Linux - Fix hlist_head object extension by dropping the iterator since it can't determine the correct member_name --- volatility3/framework/symbols/linux/extensions/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 4b1df2a0ba..acf78278b6 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -994,9 +994,6 @@ def to_list( current = current.next - def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: - return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) - class files_struct(objects.StructType): def get_fds(self) -> interfaces.objects.ObjectInterface: From 708d9330216853bfebcaa9d5799053656e422e10 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 23 Oct 2024 21:23:14 +0200 Subject: [PATCH 202/250] use additional_locals in run method --- volatility3/cli/volshell/generic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index c1ca7b5fa7..39a7aa126b 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -58,7 +58,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] ] def run( - self, additional_locals: Dict[str, Any] = None + self, additional_locals: Dict[str, Any] = {} ) -> interfaces.renderers.TreeGrid: """Runs the interactive volshell plugin. @@ -94,7 +94,10 @@ def run( """ sys.ps1 = f"({self.current_layer}) >>> " - self.__console = code.InteractiveConsole(locals=self._construct_locals_dict()) + # Dict self._construct_locals_dict() will have priority on keys + combined_locals = self._construct_locals_dict().copy() + combined_locals.update(additional_locals) + self.__console = code.InteractiveConsole(locals=combined_locals) # Since we have to do work to add the option only once for all different modes of volshell, we can't # rely on the default having been set if self.config.get("script", None) is not None: From 7f2d8eabb7f513148a7868d9a758c10f43d0450b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 23 Oct 2024 21:39:57 +0200 Subject: [PATCH 203/250] set correct locals priority --- volatility3/cli/volshell/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 39a7aa126b..a0477656e2 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -95,8 +95,8 @@ def run( sys.ps1 = f"({self.current_layer}) >>> " # Dict self._construct_locals_dict() will have priority on keys - combined_locals = self._construct_locals_dict().copy() - combined_locals.update(additional_locals) + combined_locals = additional_locals.copy() + combined_locals.update(self._construct_locals_dict()) self.__console = code.InteractiveConsole(locals=combined_locals) # Since we have to do work to add the option only once for all different modes of volshell, we can't # rely on the default having been set From 388a3e64587478b0a07121a41f2b05a684c4292e Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 24 Oct 2024 14:39:22 -0500 Subject: [PATCH 204/250] Linux: Adds missing abc.Iterable implementation `hlist_head` is missing an implementation for `collections.abc.Iterable`, resulting in a crash at runtime. This fixes the issue by providing the required `__iter__` implementation for `hlist_head`. --- volatility3/framework/symbols/linux/extensions/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 14be5ec0a9..9bd401db21 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1037,6 +1037,9 @@ def to_list( current = current.next + def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: + return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) + class files_struct(objects.StructType): def get_fds(self) -> interfaces.objects.ObjectInterface: From 4c2d2f98da5c6fcbfe8e8aa2efa590438cdf1444 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Thu, 24 Oct 2024 15:39:15 -0500 Subject: [PATCH 205/250] Removes collections.abc.Iterable superclass Per @gcmoreira this is the correct fix vs the one previously proposed, since self.vol.member_name cannot be used as the member name to iterate this type. --- volatility3/framework/symbols/linux/extensions/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 9bd401db21..eba7757140 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1004,7 +1004,7 @@ def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) -class hlist_head(objects.StructType, collections.abc.Iterable): +class hlist_head(objects.StructType): def to_list( self, symbol_type: str, @@ -1037,9 +1037,6 @@ def to_list( current = current.next - def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: - return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) - class files_struct(objects.StructType): def get_fds(self) -> interfaces.objects.ObjectInterface: From 0413cc5e2634ad4a872c96cf85e0eaacae23d921 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 25 Oct 2024 12:46:32 -0500 Subject: [PATCH 206/250] Windows: Scheduled tasks - code review fixes --- test/__init__.py | 0 test/plugins/__init__.py | 0 test/plugins/windows/__init__.py | 0 test/plugins/windows/test_scheduled_tasks.py | 387 ++++++++++++++++ .../plugins/windows/scheduled_tasks.py | 416 +----------------- 5 files changed, 406 insertions(+), 397 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/plugins/__init__.py create mode 100644 test/plugins/windows/__init__.py create mode 100644 test/plugins/windows/test_scheduled_tasks.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/plugins/__init__.py b/test/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/plugins/windows/__init__.py b/test/plugins/windows/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py new file mode 100644 index 0000000000..8f771b323c --- /dev/null +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -0,0 +1,387 @@ +import sys +import struct +import traceback +import unittest +sys.path.insert(0, "../../volatility3") +from volatility3.plugins.windows import scheduled_tasks + +class TestActionsDecoding(unittest.TestCase): + def test_decode_exe_action(self): + # fmt: off + buf = struct.pack( + "512B", + *[ + 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4c, 0x00, + 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, + 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x6d, 0x00, 0x66, 0x66, 0x00, 0x00, + 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00, + 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, 0x5c, 0x00, + 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, 0x32, 0x00, + 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, + 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, 0x65, 0x00, + 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, 0x76, 0x00, + 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x5c, 0x00, + 0x70, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x73, 0x00, 0x68, 0x00, 0x65, 0x00, + 0x6c, 0x00, 0x6c, 0x00, 0x2e, 0x00, 0x65, 0x00, + 0x78, 0x00, 0x65, 0x00, 0x62, 0x01, 0x00, 0x00, + 0x2d, 0x00, 0x45, 0x00, 0x78, 0x00, 0x65, 0x00, + 0x63, 0x00, 0x75, 0x00, 0x74, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x50, 0x00, 0x6f, 0x00, + 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x79, 0x00, + 0x20, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, 0x00, + 0x69, 0x00, 0x63, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x64, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, + 0x6f, 0x00, 0x6e, 0x00, 0x49, 0x00, 0x6e, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, + 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, + 0x6f, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00, + 0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, + 0x20, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x53, 0x00, 0x74, 0x00, 0x79, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x48, 0x00, 0x69, 0x00, + 0x64, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6e, 0x00, + 0x20, 0x00, 0x22, 0x00, 0x26, 0x00, 0x20, 0x00, + 0x25, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, + 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, + 0x5c, 0x00, 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, + 0x32, 0x00, 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x73, 0x00, 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, + 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, + 0x76, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, + 0x5c, 0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, + 0x75, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x5c, 0x00, 0x53, 0x00, 0x6d, 0x00, 0x62, 0x00, + 0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x5c, 0x00, 0x44, 0x00, 0x69, 0x00, + 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x75, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x64, 0x00, 0x53, 0x00, + 0x6d, 0x00, 0x62, 0x00, 0x31, 0x00, 0x2e, 0x00, + 0x70, 0x00, 0x73, 0x00, 0x31, 0x00, 0x20, 0x00, + 0x2d, 0x00, 0x53, 0x00, 0x63, 0x00, 0x65, 0x00, + 0x6e, 0x00, 0x61, 0x00, 0x72, 0x00, 0x69, 0x00, + 0x6f, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6c, 0x00, + 0x69, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + ) + + try: + actions = scheduled_tasks.ActionSet.decode(buf).actions # type: ignore + self.assertEqual(len(actions), 1) + self.assertEqual(actions[0].action_type, scheduled_tasks.ActionType.Exe) + except Exception: + self.fail( + "ActionDecoder.decode should not raise exception:\n%s" + % traceback.format_exc() + ) + + +class TestTriggersDecoding(unittest.TestCase): + def test_decode_all_triggers(self): + """ + Tests decoding a set of all triggers that can be constructed via the + Task Scheduler GUI interface. Ensures that the correct number of bytes + is being consumed for each trigger structure. + """ + buf = struct.pack( + "1808B", + # fmt: off + *[ + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, + 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, + 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, + 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, + 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, + 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, + 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, + 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, + 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, + 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, + 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, + 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, + 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, + 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, + 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, + 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, + 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, + 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, + 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, + 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, + 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, + 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, + 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, + 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, + 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, + 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, + 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, + 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, + 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, + 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, + 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, + 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, + 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, + 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ] + # fmt: on + ) + triggers = scheduled_tasks.TriggerSet.decode(buf) + self.assertIsNotNone(triggers) + + def test_decode_triggers(self): + # fmt: off + buf = struct.pack( + "320B", + *[ + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x08, 0xA1, 0x40, 0x42, 0x48, 0x48, 0x48, 0x48, + 0x7A, 0x7F, 0x59, 0xDC, 0x48, 0x48, 0x48, 0x48, + 0x22, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x49, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x61, 0x00, 0x63, 0x00, 0x74, 0x00, + 0x69, 0x00, 0x76, 0x00, 0x65, 0x00, 0x55, 0x00, + 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, + 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x05, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x0C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x04, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x2C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x80, 0x51, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x2C, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xD9, 0x04, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, + 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + ] + ) + # fmt: on + triggers = scheduled_tasks.TriggerSet.decode(buf) + self.assertIsNotNone(triggers) + if not triggers: + return + self.assertGreater(len(triggers.triggers), 0) diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 6d3ee586dd..374d2d47a5 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -11,8 +11,6 @@ import itertools import logging import struct -import traceback -import unittest from typing import Dict, Iterator, List, Optional, Tuple, Union from volatility3.framework import exceptions, interfaces, renderers @@ -52,6 +50,16 @@ class TimeMode(enum.Enum): Unknown = "Unknown" +TIME_MODE_DESCRIPTION = { + TimeMode.Once: "Once", + TimeMode.Daily: "Daily", + TimeMode.Weekly: "Weekly", + TimeMode.DaysInMonths: "Days In Months", + TimeMode.DaysInWeeksInMonths: "Days In Weeks In Months", + TimeMode.Unknown: "Unknown", +} + + class ActionType(enum.Enum): """ Enumeration that maps action types to their magic number encodings @@ -193,13 +201,8 @@ def read_task_scheduler_time(self) -> Optional[Tuple[bool, datetime.datetime]]: filetime = self.decode_filetime() if filetime is None: return None - return is_localized, filetime - def read_filetime(self) -> Optional[datetime.datetime]: - return datetime.datetime.now() - - def seek_relative(self, offset) -> int: - return self.seek(offset, io.SEEK_CUR) + return is_localized, filetime def read_bool(self, aligned=False) -> Optional[bool]: try: @@ -561,13 +564,14 @@ def decode(cls, reader: _ScheduledTasksReader) -> Optional["_JobSchedule"]: repetition_duration_secs = reader.read_u4() execution_time_limit_secs = reader.read_u4() mode_index = reader.read_u4() - try: - if mode_index is not None: - mode = list(TimeMode)[mode_index] - else: - mode = None - except IndexError: - mode = TimeMode.Unknown + if mode_index is not None: + try: + mode = TimeMode(mode_index) + except ValueError: + mode = TimeMode.Unknown + else: + mode = None + data1 = reader.read_u2() data2 = reader.read_u2() data3 = reader.read_u2() @@ -1412,385 +1416,3 @@ def run(self): for indent, entry in self._generator() ), ) - - -class TestActionsDecoding(unittest.TestCase): - def test_decode_exe_action(self): - # fmt: off - buf = struct.pack( - "512B", - *[ - 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4c, 0x00, - 0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00, - 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x65, 0x00, 0x6d, 0x00, 0x66, 0x66, 0x00, 0x00, - 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x25, 0x00, - 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00, - 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, 0x5c, 0x00, - 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, 0x32, 0x00, - 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, - 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, - 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, 0x65, 0x00, - 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, 0x76, 0x00, - 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x5c, 0x00, - 0x70, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x73, 0x00, 0x68, 0x00, 0x65, 0x00, - 0x6c, 0x00, 0x6c, 0x00, 0x2e, 0x00, 0x65, 0x00, - 0x78, 0x00, 0x65, 0x00, 0x62, 0x01, 0x00, 0x00, - 0x2d, 0x00, 0x45, 0x00, 0x78, 0x00, 0x65, 0x00, - 0x63, 0x00, 0x75, 0x00, 0x74, 0x00, 0x69, 0x00, - 0x6f, 0x00, 0x6e, 0x00, 0x50, 0x00, 0x6f, 0x00, - 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x79, 0x00, - 0x20, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x72, 0x00, - 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, 0x00, - 0x69, 0x00, 0x63, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x64, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, - 0x6f, 0x00, 0x6e, 0x00, 0x49, 0x00, 0x6e, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00, - 0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, 0x00, - 0x65, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00, - 0x6f, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00, - 0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, - 0x20, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, - 0x53, 0x00, 0x74, 0x00, 0x79, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x20, 0x00, 0x48, 0x00, 0x69, 0x00, - 0x64, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6e, 0x00, - 0x20, 0x00, 0x22, 0x00, 0x26, 0x00, 0x20, 0x00, - 0x25, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, - 0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x25, 0x00, - 0x5c, 0x00, 0x73, 0x00, 0x79, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, - 0x32, 0x00, 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, - 0x73, 0x00, 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x53, 0x00, 0x68, 0x00, - 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, - 0x76, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, - 0x5c, 0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, - 0x75, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x5c, 0x00, 0x53, 0x00, 0x6d, 0x00, 0x62, 0x00, - 0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00, - 0x65, 0x00, 0x5c, 0x00, 0x44, 0x00, 0x69, 0x00, - 0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x75, 0x00, - 0x73, 0x00, 0x65, 0x00, 0x64, 0x00, 0x53, 0x00, - 0x6d, 0x00, 0x62, 0x00, 0x31, 0x00, 0x2e, 0x00, - 0x70, 0x00, 0x73, 0x00, 0x31, 0x00, 0x20, 0x00, - 0x2d, 0x00, 0x53, 0x00, 0x63, 0x00, 0x65, 0x00, - 0x6e, 0x00, 0x61, 0x00, 0x72, 0x00, 0x69, 0x00, - 0x6f, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6c, 0x00, - 0x69, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] - ) - - try: - actions = ActionSet.decode(buf) # type: ignore - self.assertEqual(len(actions), 1) - self.assertEqual(actions[0].action_type, ActionType.Exe) - except Exception: - self.fail( - "ActionDecoder.decode should not raise exception:\n%s" - % traceback.format_exc() - ) - - -class TestTriggersDecoding(unittest.TestCase): - def test_decode_all_triggers(self): - """ - Tests decoding a set of all triggers that can be constructed via the - Task Scheduler GUI interface. Ensures that the correct number of bytes - is being consumed for each trigger structure. - """ - buf = struct.pack( - "1808B", - # fmt: off - *[ - 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, - 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, - 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, - 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, - 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, - 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, - 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, - 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, - 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, - 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, - 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, - 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, - 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, - 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, - 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, - 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, - 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, - 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, - 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, - 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, - 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, - 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, - 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ] - # fmt: on - ) - triggers = TriggerSet.decode(buf) - self.assertIsNotNone(triggers) - - def test_decode_triggers(self): - # fmt: off - buf = struct.pack( - "320B", - *[ - 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x08, 0xA1, 0x40, 0x42, 0x48, 0x48, 0x48, 0x48, - 0x7A, 0x7F, 0x59, 0xDC, 0x48, 0x48, 0x48, 0x48, - 0x22, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x49, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x63, 0x00, 0x74, 0x00, - 0x69, 0x00, 0x76, 0x00, 0x65, 0x00, 0x55, 0x00, - 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, - 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x05, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x0C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x04, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x2C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0x80, 0x51, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0x2C, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xD9, 0x04, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - ] - ) - # fmt: on - triggers = TriggerSet.decode(buf) - self.assertIsNotNone(triggers) - if not triggers: - return - self.assertGreater(len(triggers.triggers), 0) From efdbc27b95e141fe739b8f7e8edd1357c61b4601 Mon Sep 17 00:00:00 2001 From: David McDonald Date: Fri, 25 Oct 2024 14:06:41 -0500 Subject: [PATCH 207/250] Windows: Scheduled tasks - removes special time type Removes the TaskSchedulerTime dataclass and discards the is_localized value parsed with the timestamp. --- .../plugins/windows/scheduled_tasks.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 374d2d47a5..277a0d8563 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -137,16 +137,6 @@ class SidType(enum.Enum): LogonSession = 11 -@dataclasses.dataclass -class TaskSchedulerTime: - """ - A class containing datetime information about when a task will run - """ - - is_localized: bool - filetime: Optional[datetime.datetime] - - @dataclasses.dataclass class TaskSchedulerTimePeriod: """ @@ -196,13 +186,13 @@ class TaskSchedulerTimePeriod: class _ScheduledTasksReader(io.BytesIO): - def read_task_scheduler_time(self) -> Optional[Tuple[bool, datetime.datetime]]: - is_localized = bool(self.read_aligned_u1()) + def read_task_scheduler_time(self) -> Optional[datetime.datetime]: + _ = bool(self.read_aligned_u1()) # is_localized filetime = self.decode_filetime() if filetime is None: return None - return is_localized, filetime + return filetime def read_bool(self, aligned=False) -> Optional[bool]: try: @@ -584,8 +574,8 @@ def decode(cls, reader: _ScheduledTasksReader) -> Optional["_JobSchedule"]: reader.seek(4, io.SEEK_CUR) # pad return cls( - start_boundary[1] if start_boundary is not None else None, - end_boundary[1] if end_boundary is not None else None, + start_boundary, + end_boundary, repetition_interval_secs, repetition_duration_secs, execution_time_limit_secs, @@ -833,8 +823,8 @@ def _decode_generic_trigger( reader.seek((8 - (reader.tell() - cur)) % 8, io.SEEK_CUR) # pad to block return cls( - start_boundary[1] if start_boundary is not None else None, - end_boundary[1] if end_boundary is not None else None, + start_boundary, + end_boundary, repetition_interval_secs, trigger_enabled, trigger_type, From 094cdf18f14d795f385e504506390ba1602c0b55 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 10:59:18 +1100 Subject: [PATCH 208/250] Linux: netfilter plugin: Fix hooked field to match vol2 output --- volatility3/framework/plugins/linux/netfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index f576c073c1..66075f907d 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -190,7 +190,7 @@ def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: priority = int(hook_ops.priority) hook_ops_hook = hook_ops.hook module_name = self.get_module_name_for_address(hook_ops_hook) - hooked = module_name is not None + hooked = module_name is None yield netns, proto_name, hook_name, priority, hook_ops_hook, module_name, hooked From 4895af47bdec7779c7806dca47c657ae686a6e0b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 13:46:19 +1100 Subject: [PATCH 209/250] Linux: Boottime timeliner: Rollback timeliner event type changes and use the created time for the boot time plugin --- volatility3/framework/plugins/linux/boottime.py | 2 +- volatility3/framework/plugins/timeliner.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/linux/boottime.py b/volatility3/framework/plugins/linux/boottime.py index eeee418f85..8f63ee7f87 100644 --- a/volatility3/framework/plugins/linux/boottime.py +++ b/volatility3/framework/plugins/linux/boottime.py @@ -88,7 +88,7 @@ def generate_timeline(self): ): description = f"System boot time for time namespace {time_namespace_id}" - yield description, timeliner.TimeLinerType.BOOTTIME, boottime + yield description, timeliner.TimeLinerType.CREATED, boottime def run(self): columns = [ diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index 70da0c4fbc..c754e43eff 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -23,7 +23,6 @@ class TimeLinerType(enum.IntEnum): MODIFIED = 2 ACCESSED = 3 CHANGED = 4 - BOOTTIME = 5 class TimeLinerInterface(metaclass=abc.ABCMeta): @@ -172,10 +171,6 @@ def _generator( TimeLinerType.CHANGED, renderers.NotApplicableValue(), ), - times.get( - TimeLinerType.BOOTTIME, - renderers.NotApplicableValue(), - ), ], ) ) @@ -183,11 +178,11 @@ def _generator( # Write each entry because the body file doesn't need to be sorted if fp: times = self.timeline[(plugin_name, item)] - # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime|boottime + # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime if self._any_time_present(times): fp.write( - "|{} - {}|0|0|0|0|0|{}|{}|{}|{}|{}\n".format( + "|{} - {}|0|0|0|0|0|{}|{}|{}|{}\n".format( plugin_name, self._sanitize_body_format(item), self._text_format( @@ -202,9 +197,6 @@ def _generator( self._text_format( times.get(TimeLinerType.CREATED, "0") ), - self._text_format( - times.get(TimeLinerType.BOOTTIME, "0") - ), ) ) except Exception as e: @@ -328,7 +320,6 @@ def run(self): ("Modified Date", datetime.datetime), ("Accessed Date", datetime.datetime), ("Changed Date", datetime.datetime), - ("Boot Date", datetime.datetime), ], generator=self._generator(plugins_to_run), ) From c4274c942c4e94e909de44bbe3e12b47a15143f2 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 13:49:58 +1100 Subject: [PATCH 210/250] Linux: Fix exception message in TimespecVol3::__sub__() --- volatility3/framework/symbols/linux/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 95b4762634..8c28ca5628 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -903,7 +903,7 @@ def __sub__(self, timespec) -> "TimespecVol3": """Returns a new TimespecVol3 object that subtracts the values in the timespec argument from the current object's values""" if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): - raise TypeError("Cannot add a TimespecVol3 to this object") + raise TypeError("Cannot substract this object to a TimespecVol3") result = TimespecVol3( tv_sec=self.tv_sec - timespec.tv_sec, From 57ffd5b939df456b7e573d91d6b23bc01f36f353 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 14:24:16 +1100 Subject: [PATCH 211/250] Linux: Boottime API: Refactor TimespecVol3::negate() to return a new object instead of modifying the original. It also normalizes its values, aligning with the behavior of the other addition and subtraction operators --- volatility3/framework/symbols/linux/__init__.py | 14 +++++++++++--- .../framework/symbols/linux/extensions/__init__.py | 10 ++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 8c28ca5628..4123674ed7 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -909,6 +909,7 @@ def __sub__(self, timespec) -> "TimespecVol3": tv_sec=self.tv_sec - timespec.tv_sec, tv_nsec=self.tv_nsec - timespec.tv_nsec, ) + result.normalize() return result @@ -925,6 +926,13 @@ def normalize(self): self.tv_sec -= 1 def negate(self): - """Negates the sign of both tv_sec and tv_nsec""" - self.tv_sec = -self.tv_sec - self.tv_nsec = -self.tv_nsec + """Returns a new TimespecVol3 object with the values of the current object negated""" + + result = TimespecVol3( + tv_sec=-self.tv_sec, + tv_nsec=-self.tv_nsec, + ) + + result.normalize() + + return result diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2784aeda70..6fd55cb565 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -502,10 +502,7 @@ def _get_boottime_raw(self) -> "linux.TimespecVol3": boottime += timekeeper.total_sleep_time - boottime.negate() - boottime.normalize() - - return boottime + return boottime.negate() elif vmlinux.has_symbol("wall_to_monotonic"): # kernels < 3.4 - Tested on Debian7 3.2.0-4 (3.2.57-3+deb7u2) @@ -523,10 +520,7 @@ def _get_boottime_raw(self) -> "linux.TimespecVol3": # kernels < 2.6.32 total_sleep_time is an unsigned long as seconds boottime.tv_sec += total_sleep_time - boottime.negate() - boottime.normalize() - - return boottime + return boottime.negate() raise exceptions.VolatilityException("Unsupported") From bee2a398eb4d3f3b28644ed2039bb516ccc3e75e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 15:13:26 +1100 Subject: [PATCH 212/250] Linux: Minor: Add comment/header on each set of constants --- volatility3/framework/constants/linux/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 056c384ae5..7bfa9268da 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -304,6 +304,7 @@ class ELF_CLASS(IntEnum): ELFCLASS64 = 2 +# PTrace PT_OPT_FLAG_SHIFT = 3 PTRACE_EVENT_FORK = 1 @@ -341,4 +342,5 @@ def flags(self) -> str: return str(self).replace(self.__class__.__name__ + ".", "") +# Boot time NSEC_PER_SEC = 1e9 From 526007f948b4b1fcefacb84e344d0b6ff49e945b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 16:41:47 +1100 Subject: [PATCH 213/250] Linux: hidden_modules: Use child_template --- volatility3/framework/plugins/linux/hidden_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 679f2b00a3..09b8851ec3 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -167,7 +167,7 @@ def get_hidden_modules( mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") offset_to_mkobj_mod = mkobj_offset + mod_offset - mod_member_template = vmlinux.get_type("module_kobject").vol.members["mod"][1] + mod_member_template = vmlinux.get_type("module_kobject").child_template("mod") mod_size = mod_member_template.size mod_member_data_format = mod_member_template.data_format From cbe071ff3ef90c4c2ae78bc953390bd69deb7c02 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 17:19:30 +1100 Subject: [PATCH 214/250] Linux: hidden_modules: Import the whole architectures module --- volatility3/framework/plugins/linux/hidden_modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 09b8851ec3..b7513a952c 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -4,7 +4,7 @@ import logging from typing import List, Set, Tuple, Iterable from volatility3.framework import renderers, interfaces, exceptions, objects -from volatility3.framework.constants.architectures import LINUX_ARCHS +from volatility3.framework.constants import architectures from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.plugins.linux import lsmod @@ -25,7 +25,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.ModuleRequirement( name="kernel", description="Linux kernel", - architectures=LINUX_ARCHS, + architectures=architectures.LINUX_ARCHS, ), requirements.PluginRequirement( name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) From dfd8a1f19565ddfdd9521206bf4fb9773ba52578 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 17:22:48 +1100 Subject: [PATCH 215/250] Linux: hidden_modules: Include kernel version and commit details --- volatility3/framework/plugins/linux/hidden_modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index b7513a952c..7fd5f9fad7 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -48,10 +48,12 @@ def get_modules_memory_boundaries( """ vmlinux = context.modules[vmlinux_module_name] if vmlinux.has_symbol("mod_tree"): + # Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca mod_tree = vmlinux.object_from_symbol("mod_tree") modules_addr_min = mod_tree.addr_min modules_addr_max = mod_tree.addr_max elif vmlinux.has_symbol("module_addr_min"): + # 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db modules_addr_min = vmlinux.object_from_symbol("module_addr_min") modules_addr_max = vmlinux.object_from_symbol("module_addr_max") From 1c6a5480170d38760f53e3d1caa2a6dafcba2f46 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 17:30:47 +1100 Subject: [PATCH 216/250] Linux: hidden_modules: Simplify symbols type checks --- .../framework/plugins/linux/hidden_modules.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 7fd5f9fad7..7a01341c15 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -58,21 +58,10 @@ def get_modules_memory_boundaries( modules_addr_max = vmlinux.object_from_symbol("module_addr_max") if isinstance(modules_addr_min, objects.Void): - # Crap ISF! Here's my best-effort workaround - vollog.warning( - "Your ISF symbols are missing type information. You may need to update " - "the ISF using the latest version of dwarf2json" + raise exceptions.VolatilityException( + "Your ISF symbols lack type information. You may need to update the" + "ISF using the latest version of dwarf2json" ) - # See issue #1041. In the Linux kernel these are "unsigned long" - for type_name in ("long unsigned int", "unsigned long"): - if vmlinux.has_type(type_name): - modules_addr_min = modules_addr_min.cast(type_name) - modules_addr_max = modules_addr_max.cast(type_name) - break - else: - raise exceptions.VolatilityException( - "Bad ISF! Please update the ISF using the latest version of dwarf2json" - ) else: raise exceptions.VolatilityException( "Cannot find the module memory allocation area. Unsupported kernel" From 8960bdab086bef47121c16ba92a630dd70a18a26 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 29 Oct 2024 17:33:06 +1100 Subject: [PATCH 217/250] Linux: hidden_modules: Add a symbol table check for a recent dwarf2json version --- volatility3/framework/plugins/linux/hidden_modules.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index 7a01341c15..fd4b289430 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -232,6 +232,13 @@ def _generator(self): yield (0, fields) def run(self): + if self.context.symbol_space.verify_table_versions( + "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) + ): + raise exceptions.SymbolSpaceError( + "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" + ) + headers = [ ("Address", format_hints.Hex), ("Name", str), From 4b76b699ad97f93fd9cf2949e6ab49c526784f3c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 09:56:05 +1100 Subject: [PATCH 218/250] Linux: hidden_modules: Add docstrings and comments to enhance the documentation of the module verification process. Move the hardcoded values to the linux constants file. --- .../framework/constants/linux/__init__.py | 8 ++++ .../symbols/linux/extensions/__init__.py | 37 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 0567b8574c..7c485d3c39 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -339,3 +339,11 @@ class PT_FLAGS(Flag): def flags(self) -> str: """Returns the ptrace flags string""" return str(self).replace(self.__class__.__name__ + ".", "") + + +# Valid sizes for modules. Note that the Linux kernel does not define these values; they +# are based on empirical observations of typical memory allocations for kernel modules. +# We use this to verify that the found module falls within reasonable limits. +MODULE_MAXIMUM_CORE_SIZE = 20000000 +MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000 +MODULE_MINIMUM_SIZE = 4096 diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index cc43df8f51..fedb3d96fc 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,12 +13,24 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols 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 -from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES -from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES, PT_FLAGS +from volatility3.framework.constants.linux import ( + SOCK_TYPES, + SOCK_FAMILY, + IP_PROTOCOLS, + IPV6_PROTOCOLS, + TCP_STATES, + NETLINK_PROTOCOLS, + ETH_PROTOCOLS, + BLUETOOTH_STATES, + BLUETOOTH_PROTOCOLS, + SOCKET_STATES, + CAPABILITIES, + PT_FLAGS, + MODULE_MAXIMUM_CORE_SIZE, + MODULE_MAXIMUM_CORE_TEXT_SIZE, + MODULE_MINIMUM_SIZE, +) + from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed @@ -36,16 +48,23 @@ def __init__(self, *args, **kwargs): self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization def is_valid(self): + """Determine whether it is a valid module object by verifying the self-referential + in module_kobject. This also confirms that the module is actively allocated and + not a remnant of freed memory or a failed module load attempt by verifying the + module memory section sizes. + """ layer = self._context.layers[self.vol.layer_name] # Make sure the entire module content is readable if not layer.is_valid(self.vol.offset, self.vol.size): return False core_size = self.get_core_size() + core_text_size = self.get_core_text_size() + init_size = self.get_init_size() if not ( - 1 <= core_size <= 20000000 - and core_size + self.get_init_size() >= 4096 - and 1 <= self.get_core_text_size() <= 20000000 + 0 < core_text_size <= MODULE_MAXIMUM_CORE_TEXT_SIZE + and 0 < core_size <= MODULE_MAXIMUM_CORE_SIZE + and core_size + init_size >= MODULE_MINIMUM_SIZE ): return False From 722ccd527e351f90d2a34dd31d29c39bf65be513 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 12:56:09 +1100 Subject: [PATCH 219/250] Linux: Extensions: Clean up the Linux constants imports in the object extension file --- .../symbols/linux/extensions/__init__.py | 83 ++++++++----------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index fedb3d96fc..1f3e0504c2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -13,24 +13,7 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols from volatility3.framework.renderers import conversion -from volatility3.framework.constants.linux import ( - SOCK_TYPES, - SOCK_FAMILY, - IP_PROTOCOLS, - IPV6_PROTOCOLS, - TCP_STATES, - NETLINK_PROTOCOLS, - ETH_PROTOCOLS, - BLUETOOTH_STATES, - BLUETOOTH_PROTOCOLS, - SOCKET_STATES, - CAPABILITIES, - PT_FLAGS, - MODULE_MAXIMUM_CORE_SIZE, - MODULE_MAXIMUM_CORE_TEXT_SIZE, - MODULE_MINIMUM_SIZE, -) - +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed @@ -62,9 +45,9 @@ def is_valid(self): core_text_size = self.get_core_text_size() init_size = self.get_init_size() if not ( - 0 < core_text_size <= MODULE_MAXIMUM_CORE_TEXT_SIZE - and 0 < core_size <= MODULE_MAXIMUM_CORE_SIZE - and core_size + init_size >= MODULE_MINIMUM_SIZE + 0 < core_text_size <= linux_constants.MODULE_MAXIMUM_CORE_TEXT_SIZE + and 0 < core_size <= linux_constants.MODULE_MAXIMUM_CORE_SIZE + and core_size + init_size >= linux_constants.MODULE_MINIMUM_SIZE ): return False @@ -383,7 +366,7 @@ def is_kernel_thread(self) -> bool: Returns: bool: True, if this task is a kernel thread. Otherwise, False. """ - return (self.flags & constants.linux.PF_KTHREAD) != 0 + return (self.flags & linux_constants.PF_KTHREAD) != 0 @property def is_thread_group_leader(self) -> bool: @@ -460,7 +443,11 @@ def get_ptrace_tracee_tids(self) -> List[int]: def get_ptrace_tracee_flags(self) -> Optional[str]: """Returns a string with the ptrace flags""" - return PT_FLAGS(self.ptrace).flags if self.is_being_ptraced else None + return ( + linux_constants.PT_FLAGS(self.ptrace).flags + if self.is_being_ptraced + else None + ) class fs_struct(objects.StructType): @@ -1567,18 +1554,18 @@ def get_inode(self): def get_state(self): socket_state_idx = self.state - if 0 <= socket_state_idx < len(SOCKET_STATES): - return SOCKET_STATES[socket_state_idx] + if 0 <= socket_state_idx < len(linux_constants.SOCKET_STATES): + return linux_constants.SOCKET_STATES[socket_state_idx] class sock(objects.StructType): def get_family(self): family_idx = self.__sk_common.skc_family - if 0 <= family_idx < len(SOCK_FAMILY): - return SOCK_FAMILY[family_idx] + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] def get_type(self): - return SOCK_TYPES.get(self.sk_type, "") + return linux_constants.SOCK_TYPES.get(self.sk_type, "") def get_inode(self): if not self.sk_socket: @@ -1612,8 +1599,8 @@ def get_state(self): # Unix socket states reuse (a subset) of the inet_sock states contants if self.sk.get_type() == "STREAM": state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(TCP_STATES): - return TCP_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] else: # Return the generic socket state return self.sk.sk_socket.get_state() @@ -1625,15 +1612,15 @@ def get_inode(self): class inet_sock(objects.StructType): def get_family(self): family_idx = self.sk.__sk_common.skc_family - if 0 <= family_idx < len(SOCK_FAMILY): - return SOCK_FAMILY[family_idx] + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] def get_protocol(self): # If INET6 family and a proto is defined, we use that specific IPv6 protocol. # Otherwise, we use the standard IP protocol. - protocol = IP_PROTOCOLS.get(self.sk.sk_protocol) + protocol = linux_constants.IP_PROTOCOLS.get(self.sk.sk_protocol) if self.get_family() == "AF_INET6": - protocol = IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) + protocol = linux_constants.IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) return protocol def get_state(self): @@ -1641,8 +1628,8 @@ def get_state(self): if self.sk.get_type() == "STREAM": state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(TCP_STATES): - return TCP_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] else: # Return the generic socket state return self.sk.sk_socket.get_state() @@ -1725,8 +1712,8 @@ def get_dst_addr(self): class netlink_sock(objects.StructType): def get_protocol(self): protocol_idx = self.sk.sk_protocol - if 0 <= protocol_idx < len(NETLINK_PROTOCOLS): - return NETLINK_PROTOCOLS[protocol_idx] + if 0 <= protocol_idx < len(linux_constants.NETLINK_PROTOCOLS): + return linux_constants.NETLINK_PROTOCOLS[protocol_idx] def get_state(self): # Return the generic socket state @@ -1768,8 +1755,8 @@ def get_protocol(self): eth_proto = socket_module.htons(self.num) if eth_proto == 0: return None - elif eth_proto in ETH_PROTOCOLS: - return ETH_PROTOCOLS[eth_proto] + elif eth_proto in linux_constants.ETH_PROTOCOLS: + return linux_constants.ETH_PROTOCOLS[eth_proto] else: return f"0x{eth_proto:x}" @@ -1781,13 +1768,13 @@ def get_state(self): class bt_sock(objects.StructType): def get_protocol(self): type_idx = self.sk.sk_protocol - if 0 <= type_idx < len(BLUETOOTH_PROTOCOLS): - return BLUETOOTH_PROTOCOLS[type_idx] + if 0 <= type_idx < len(linux_constants.BLUETOOTH_PROTOCOLS): + return linux_constants.BLUETOOTH_PROTOCOLS[type_idx] def get_state(self): state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(BLUETOOTH_STATES): - return BLUETOOTH_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.BLUETOOTH_STATES): + return linux_constants.BLUETOOTH_STATES[state_idx] class xdp_sock(objects.StructType): @@ -1905,7 +1892,7 @@ def get_last_cap_value(cls) -> int: Returns: int: The latest capability ID supported by the framework. """ - return len(CAPABILITIES) - 1 + return len(linux_constants.CAPABILITIES) - 1 def get_kernel_cap_full(self) -> int: """Return the maximum value allowed for this kernel for a capability @@ -1934,7 +1921,7 @@ def capabilities_to_string(cls, capabilities_bitfield: int) -> List[str]: """ capabilities = [] - for bit, name in enumerate(CAPABILITIES): + for bit, name in enumerate(linux_constants.CAPABILITIES): if capabilities_bitfield & (1 << bit) != 0: capabilities.append(name) @@ -1995,10 +1982,10 @@ def has_capability(self, capability: str) -> bool: Returns: bool: "True" if the given capability is enabled. """ - if capability not in CAPABILITIES: + if capability not in linux_constants.CAPABILITIES: raise AttributeError(f"Unknown capability with name '{capability}'") - cap_value = 1 << CAPABILITIES.index(capability) + cap_value = 1 << linux_constants.CAPABILITIES.index(capability) return cap_value & self.get_capabilities() != 0 From 42d918c05d6a2d08d025d63f51b9c6764076be12 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 15:21:13 +1100 Subject: [PATCH 220/250] Linux: Boottime API: Refactor Timespec Methods. Move TimespecVol3 methods to an abstract class Timespec64Abstract, which is now inherited by Timespec64 and Timespec64Concrete. --- .../framework/symbols/linux/__init__.py | 106 -------------- .../symbols/linux/extensions/__init__.py | 138 ++++++++++++++++-- 2 files changed, 126 insertions(+), 118 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 84f546a9f5..1f2812c106 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -3,15 +3,11 @@ # import math import contextlib -import datetime -import dataclasses from abc import ABC, abstractmethod from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects -from volatility3.framework.renderers import conversion -from volatility3.framework.constants.linux import NSEC_PER_SEC from volatility3.framework.objects import utility from volatility3.framework.symbols import intermed from volatility3.framework.symbols.linux import extensions @@ -836,105 +832,3 @@ def get_cached_pages(self) -> interfaces.objects.ObjectInterface: page = self.vmlinux.object("page", offset=page_addr, absolute=True) if page: yield page - - -@dataclasses.dataclass -class TimespecVol3(object): - """Internal helper class to handle all required timespec operations, convertions and - adjustments. - - NOTE: This is intended for exclusive use with get_boottime() and its related functions. - """ - - tv_sec: int = 0 - tv_nsec: int = 0 - - @classmethod - def new_from_timespec(cls, timespec) -> "TimespecVol3": - """Creates a new instance from a TimespecVol3 or timespec64 object""" - if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): - raise TypeError("It requires either a TimespecVol3 or timespec64 type") - - tv_sec = int(timespec.tv_sec) - tv_nsec = int(timespec.tv_nsec) - return cls(tv_sec=tv_sec, tv_nsec=tv_nsec) - - @classmethod - def new_from_nsec(cls, nsec) -> "TimespecVol3": - """Creates a new instance from an integer in nanoseconds""" - - # Based on ns_to_timespec64() - if nsec > 0: - tv_sec = nsec // NSEC_PER_SEC - tv_nsec = nsec % NSEC_PER_SEC - elif nsec < 0: - tv_sec = -((-nsec - 1) // NSEC_PER_SEC) - 1 - rem = (-nsec - 1) % NSEC_PER_SEC - tv_nsec = NSEC_PER_SEC - rem - 1 - else: - tv_sec = tv_nsec = 0 - - return cls(tv_sec=tv_sec, tv_nsec=tv_nsec) - - def to_datetime(self) -> datetime.datetime: - """Converts this TimespecVol3 to a UTC aware datetime""" - return conversion.unixtime_to_datetime( - self.tv_sec + self.tv_nsec / NSEC_PER_SEC - ) - - def to_timedelta(self) -> datetime.timedelta: - """Converts this TimespecVol3 to timedelta""" - return datetime.timedelta(seconds=self.tv_sec + self.tv_nsec / NSEC_PER_SEC) - - def __add__(self, timespec) -> "TimespecVol3": - """Returns a new TimespecVol3 object that sums the current values with those - in the timespec argument""" - if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): - raise TypeError("Cannot add a TimespecVol3 to this object") - - result = TimespecVol3( - tv_sec=self.tv_sec + timespec.tv_sec, - tv_nsec=self.tv_nsec + timespec.tv_nsec, - ) - - result.normalize() - - return result - - def __sub__(self, timespec) -> "TimespecVol3": - """Returns a new TimespecVol3 object that subtracts the values in the timespec - argument from the current object's values""" - if not isinstance(timespec, (TimespecVol3, extensions.timespec64)): - raise TypeError("Cannot substract this object to a TimespecVol3") - - result = TimespecVol3( - tv_sec=self.tv_sec - timespec.tv_sec, - tv_nsec=self.tv_nsec - timespec.tv_nsec, - ) - - result.normalize() - - return result - - def normalize(self): - """Normalize any overflow in tv_sec and tv_nsec after previous addition or subtractions""" - # Based on kernel's set_normalized_timespec64() - while self.tv_nsec >= NSEC_PER_SEC: - self.tv_nsec -= NSEC_PER_SEC - self.tv_sec += 1 - - while self.tv_nsec < 0: - self.tv_nsec += NSEC_PER_SEC - self.tv_sec -= 1 - - def negate(self): - """Returns a new TimespecVol3 object with the values of the current object negated""" - - result = TimespecVol3( - tv_sec=-self.tv_sec, - tv_nsec=-self.tv_nsec, - ) - - result.normalize() - - return result diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index e952ed9686..ea20850b73 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import abc import collections.abc import logging import functools @@ -18,12 +19,13 @@ from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES, PT_FLAGS +from volatility3.framework.constants.linux import CAPABILITIES, PT_FLAGS, NSEC_PER_SEC from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed from volatility3.framework.symbols.linux.extensions import elf + vollog = logging.getLogger(__name__) # Keep these in a basic module, to prevent import cycles when symbol providers require them @@ -431,10 +433,10 @@ def _get_task_start_time(self) -> datetime.timedelta: if start_time_obj_type_name != "timespec": # kernels >= 3.17 real_start_time and start_time are u64 # kernels >= 5.5 uses start_boottime which is also a u64 - start_time = linux.TimespecVol3.new_from_nsec(start_time_obj) + start_time = Timespec64Concrete.new_from_nsec(start_time_obj) else: # kernels < 3.17 real_start_time and start_time are timespec - start_time = linux.TimespecVol3.new_from_timespec(start_time_obj) + start_time = Timespec64Concrete.new_from_timespec(start_time_obj) # This is relative to the boot time so it makes sense to be a timedelta. return start_time.to_timedelta() @@ -508,8 +510,8 @@ def _get_time_namespace_boottime_offset( return time_namespace_offsets.boottime - def _get_boottime_raw(self) -> "linux.TimespecVol3": - """Returns the boot time in a TimespecVol3.""" + def _get_boottime_raw(self) -> "Timespec64Concrete": + """Returns the boot time in a Timespec64Concrete object.""" vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if vmlinux.has_symbol("tk_core"): @@ -522,7 +524,7 @@ def _get_boottime_raw(self) -> "linux.TimespecVol3": else: # 3.17 <= kernels < 4.10 - Tested on Ubuntu 4.4.0-142 boottime_nsec = timekeeper.offs_real.tv64 - timekeeper.offs_boot.tv64 - return linux.TimespecVol3.new_from_nsec(boottime_nsec) + return Timespec64Concrete.new_from_nsec(boottime_nsec) elif vmlinux.has_symbol("timekeeper") and vmlinux.get_type( "timekeeper" @@ -531,7 +533,7 @@ def _get_boottime_raw(self) -> "linux.TimespecVol3": timekeeper = vmlinux.object_from_symbol("timekeeper") # timekeeper.wall_to_monotonic is timespec - boottime = linux.TimespecVol3.new_from_timespec( + boottime = Timespec64Concrete.new_from_timespec( timekeeper.wall_to_monotonic ) @@ -542,7 +544,7 @@ def _get_boottime_raw(self) -> "linux.TimespecVol3": elif vmlinux.has_symbol("wall_to_monotonic"): # kernels < 3.4 - Tested on Debian7 3.2.0-4 (3.2.57-3+deb7u2) wall_to_monotonic = vmlinux.object_from_symbol("wall_to_monotonic") - boottime = linux.TimespecVol3.new_from_timespec(wall_to_monotonic) + boottime = Timespec64Concrete.new_from_timespec(wall_to_monotonic) if vmlinux.has_symbol("total_sleep_time"): # 2.6.23 <= kernels < 3.4 7c3f1a573237b90ef331267260358a0ec4ac9079 total_sleep_time = vmlinux.object_from_symbol("total_sleep_time") @@ -2168,12 +2170,124 @@ def get_capabilities(self) -> int: return cap_value & self.get_kernel_cap_full() -class timespec64(objects.StructType): +class Timespec64Abstract(abc.ABC): + """Abstract class to handle all required timespec64 operations, convertions and + adjustments.""" + + @classmethod + def new_from_timespec(cls, other) -> "Timespec64Concrete": + """Creates a new instance from an Timespec64Abstract subclass object""" + if not isinstance(other, Timespec64Abstract): + raise TypeError("Requires an object subclass of Timespec64Abstract") + + tv_sec = int(other.tv_sec) + tv_nsec = int(other.tv_nsec) + return Timespec64Concrete(tv_sec=tv_sec, tv_nsec=tv_nsec) + + @classmethod + def new_from_nsec(cls, nsec) -> "Timespec64Concrete": + """Creates a new instance from an integer in nanoseconds""" + + # Based on ns_to_timespec64() + if nsec > 0: + tv_sec = nsec // NSEC_PER_SEC + tv_nsec = nsec % NSEC_PER_SEC + elif nsec < 0: + tv_sec = -((-nsec - 1) // NSEC_PER_SEC) - 1 + rem = (-nsec - 1) % NSEC_PER_SEC + tv_nsec = NSEC_PER_SEC - rem - 1 + else: + tv_sec = tv_nsec = 0 + + return Timespec64Concrete(tv_sec=tv_sec, tv_nsec=tv_nsec) + def to_datetime(self) -> datetime.datetime: - """Returns the respective aware datetime""" + """Converts this Timespec64Abstract subclass object to a UTC aware datetime""" + + # pylint: disable=E1101 + return conversion.unixtime_to_datetime( + self.tv_sec + self.tv_nsec / NSEC_PER_SEC + ) + + def to_timedelta(self) -> datetime.timedelta: + """Converts this Timespec64Abstract subclass object to timedelta""" + # pylint: disable=E1101 + return datetime.timedelta(seconds=self.tv_sec + self.tv_nsec / NSEC_PER_SEC) + + def __add__(self, other) -> "Timespec64Concrete": + """Returns a new Timespec64Concrete object that sums the current values with those + in the timespec argument""" + if not isinstance(other, Timespec64Abstract): + raise TypeError("Requires an object subclass of Timespec64Abstract") + + # pylint: disable=E1101 + result = Timespec64Concrete( + tv_sec=self.tv_sec + other.tv_sec, + tv_nsec=self.tv_nsec + other.tv_nsec, + ) + + result.normalize() + + return result + + def __sub__(self, other) -> "Timespec64Concrete": + """Returns a new Timespec64Abstract object that subtracts the values in the timespec + argument from the current object's values""" + if not isinstance(other, Timespec64Abstract): + raise TypeError("Requires an object subclass of Timespec64Abstract") + + # pylint: disable=E1101 + result = Timespec64Concrete( + tv_sec=self.tv_sec - other.tv_sec, + tv_nsec=self.tv_nsec - other.tv_nsec, + ) + + result.normalize() + + return result + + def normalize(self): + """Normalize any overflow in tv_sec and tv_nsec.""" + # Based on kernel's set_normalized_timespec64() + + # pylint: disable=E1101 + while self.tv_nsec >= NSEC_PER_SEC: + self.tv_nsec -= NSEC_PER_SEC + self.tv_sec += 1 + + while self.tv_nsec < 0: + self.tv_nsec += NSEC_PER_SEC + self.tv_sec -= 1 + + def negate(self): + """Returns a new Timespec64Concrete object with the values of the current object negated""" + # pylint: disable=E1101 + result = Timespec64Concrete( + tv_sec=-self.tv_sec, + tv_nsec=-self.tv_nsec, + ) + + result.normalize() + + return result + + +class Timespec64Concrete(Timespec64Abstract): + """Handle all required timespec64 operations, convertions and adjustments. + This is used to dynamically create timespec64-like objects, each its own variables + and the same methods as a timespec64 object extension. + """ + + def __init__(self, tv_sec=0, tv_nsec=0): + self.tv_sec = tv_sec + self.tv_nsec = tv_nsec + - dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9) - return dt +class timespec64(Timespec64Abstract, objects.StructType): + """Handle all required timespec64 operations, convertions and adjustments. + This works as an extension of the timespec64 object while maintaining the same methods + as a Timespec64Concrete object. + """ class inode(objects.StructType): From 7abe92cf2f2ddac1a5e7f15a27afe341e1810901 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 15:36:46 +1100 Subject: [PATCH 221/250] Linux: Boottime API: Minor. Move negate() up --- .../symbols/linux/extensions/__init__.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index ea20850b73..f20d937550 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2244,6 +2244,18 @@ def __sub__(self, other) -> "Timespec64Concrete": result.normalize() + return self + other.negate() + + def negate(self): + """Returns a new Timespec64Concrete object with the values of the current object negated""" + # pylint: disable=E1101 + result = Timespec64Concrete( + tv_sec=-self.tv_sec, + tv_nsec=-self.tv_nsec, + ) + + result.normalize() + return result def normalize(self): @@ -2259,18 +2271,6 @@ def normalize(self): self.tv_nsec += NSEC_PER_SEC self.tv_sec -= 1 - def negate(self): - """Returns a new Timespec64Concrete object with the values of the current object negated""" - # pylint: disable=E1101 - result = Timespec64Concrete( - tv_sec=-self.tv_sec, - tv_nsec=-self.tv_nsec, - ) - - result.normalize() - - return result - class Timespec64Concrete(Timespec64Abstract): """Handle all required timespec64 operations, convertions and adjustments. From e197fbaf5fe7a7fba8fabce73533725af9110d74 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 15:39:40 +1100 Subject: [PATCH 222/250] Linux: Boottime API: Refactor __sub__ to operate through __add__() and negate() for improved clarity and reuse --- .../framework/symbols/linux/extensions/__init__.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f20d937550..c80ffa5150 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2236,17 +2236,9 @@ def __sub__(self, other) -> "Timespec64Concrete": if not isinstance(other, Timespec64Abstract): raise TypeError("Requires an object subclass of Timespec64Abstract") - # pylint: disable=E1101 - result = Timespec64Concrete( - tv_sec=self.tv_sec - other.tv_sec, - tv_nsec=self.tv_nsec - other.tv_nsec, - ) - - result.normalize() - return self + other.negate() - def negate(self): + def negate(self) -> "Timespec64Concrete": """Returns a new Timespec64Concrete object with the values of the current object negated""" # pylint: disable=E1101 result = Timespec64Concrete( From a05397e8b68c280ba74c58bc968cc6692d25e613 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 15:57:00 +1100 Subject: [PATCH 223/250] Linux: Boottime API: Minor. Fix docstring typo --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index c80ffa5150..a9127907e0 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2266,7 +2266,7 @@ def normalize(self): class Timespec64Concrete(Timespec64Abstract): """Handle all required timespec64 operations, convertions and adjustments. - This is used to dynamically create timespec64-like objects, each its own variables + This is used to dynamically create timespec64-like objects, each with its own variables and the same methods as a timespec64 object extension. """ From cf94b078cdd2534493209c08c5b926c85dbcd699 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 16:11:03 +1100 Subject: [PATCH 224/250] Linux: PageCache API: Fix typing --- volatility3/framework/symbols/linux/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 1f2812c106..3289775b6e 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -615,7 +615,7 @@ def _slot_to_nodep(self, slot) -> int: return nodep - def _iter_node(self, nodep, height) -> int: + def _iter_node(self, nodep, height) -> Iterator[int]: node = self.nodep_to_node(nodep) node_slots = node.slots for off in range(self.CHUNK_SIZE): @@ -632,7 +632,7 @@ def _iter_node(self, nodep, height) -> int: for child_node in self._iter_node(nodep, height - 1): yield child_node - def get_entries(self, root: interfaces.objects.ObjectInterface) -> int: + def get_entries(self, root: interfaces.objects.ObjectInterface) -> Iterator[int]: """Walks the tree data structure Args: @@ -818,7 +818,7 @@ def __init__( self._page_cache = page_cache self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name) - def get_cached_pages(self) -> interfaces.objects.ObjectInterface: + def get_cached_pages(self) -> Iterator[interfaces.objects.ObjectInterface]: """Returns all page cache contents Yields: From 84a2233d04b9ef81c97467765a61f12e0403837e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 16:25:28 +1100 Subject: [PATCH 225/250] Linux: vfsmount type: Fix type comparison --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index eba7757140..f3c29043fc 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1299,7 +1299,7 @@ def is_equal(self, vfsmount_ptr) -> bool: bool: 'True' if the given argument points to the the same 'vfsmount' as 'self'. """ - if type(vfsmount_ptr) == objects.Pointer: + if isinstance(vfsmount_ptr, objects.Pointer): return self.vol.offset == vfsmount_ptr else: raise exceptions.VolatilityException( From aecf8d432706bff371632c28f82c9f9f025da2e6 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 18:53:20 +1100 Subject: [PATCH 226/250] Linux: module extension object: Refactor to replace custom property cache with functools.cached_property that's available from Python 3.8 --- .../symbols/linux/extensions/__init__.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index eba7757140..7dce162677 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -31,29 +31,24 @@ class module(generic.GenericIntelProcess): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization - - @property + @functools.cached_property def mod_mem_type(self): """Return the mod_mem_type enum choices if available or an empty dict if not""" # mod_mem_type and module_memory were added in kernel 6.4 which replaces # module_layout for storing the information around core_layout etc. # see commit ac3b43283923440900b4f36ca5f9f0b1ca43b70e for more information + symbol_table_name = self.get_symbol_table_name() + mod_mem_type_symname = symbol_table_name + constants.BANG + "mod_mem_type" + symbol_space = self._context.symbol_space + try: + mod_mem_type = symbol_space.get_enumeration(mod_mem_type_symname).choices + except exceptions.SymbolError: + mod_mem_type = {} + vollog.debug( + "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" + ) - if self._mod_mem_type is None: - try: - self._mod_mem_type = self._context.symbol_space.get_enumeration( - self.get_symbol_table_name() + constants.BANG + "mod_mem_type" - ).choices - except exceptions.SymbolError: - vollog.debug( - "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" - ) - # set to empty dict to show that the enum was not found, and so shouldn't be searched for again - self._mod_mem_type = {} - return self._mod_mem_type + return mod_mem_type def get_module_base(self): if self.has_member("mem"): # kernels 6.4+ From b09859668751c992be2065d60ebe2220cb4d93cc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 30 Oct 2024 19:50:13 +1100 Subject: [PATCH 227/250] Linux: module extension object: Add type annotation to the mod_mem_type property --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 7dce162677..fe3c7af495 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -32,7 +32,7 @@ class module(generic.GenericIntelProcess): @functools.cached_property - def mod_mem_type(self): + def mod_mem_type(self) -> Dict: """Return the mod_mem_type enum choices if available or an empty dict if not""" # mod_mem_type and module_memory were added in kernel 6.4 which replaces # module_layout for storing the information around core_layout etc. From e183f81de6cbea7725ac0fc2953b6b5003efb945 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Thu, 31 Oct 2024 08:28:07 +1100 Subject: [PATCH 228/250] linux: hidden_modules: Fix mess with merge #1283 --- .../symbols/linux/extensions/__init__.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 339896e9de..aa3e8c675c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -63,19 +63,18 @@ def mod_mem_type(self) -> Dict: # mod_mem_type and module_memory were added in kernel 6.4 which replaces # module_layout for storing the information around core_layout etc. # see commit ac3b43283923440900b4f36ca5f9f0b1ca43b70e for more information + symbol_table_name = self.get_symbol_table_name() + mod_mem_type_symname = symbol_table_name + constants.BANG + "mod_mem_type" + symbol_space = self._context.symbol_space + try: + mod_mem_type = symbol_space.get_enumeration(mod_mem_type_symname).choices + except exceptions.SymbolError: + mod_mem_type = {} + vollog.debug( + "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" + ) - if self._mod_mem_type is None: - try: - self._mod_mem_type = self._context.symbol_space.get_enumeration( - self.get_symbol_table_name() + constants.BANG + "mod_mem_type" - ).choices - except exceptions.SymbolError: - vollog.debug( - "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" - ) - # set to empty dict to show that the enum was not found, and so shouldn't be searched for again - self._mod_mem_type = {} - return self._mod_mem_type + return mod_mem_type def _get_mem_type(self, mod_mem_type_name): module_mem_index = self.mod_mem_type.get(mod_mem_type_name) From 2efb4e7d28d60a337aabae448de24da37c3feb7e Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 5 Nov 2024 19:56:15 +1100 Subject: [PATCH 229/250] Merge branch 'develop' into linux_boottime_support --- .../framework/constants/linux/__init__.py | 8 + .../framework/plugins/linux/hidden_modules.py | 246 ++++++++++++++++++ .../framework/symbols/linux/__init__.py | 6 +- .../symbols/linux/extensions/__init__.py | 225 +++++++++------- 4 files changed, 383 insertions(+), 102 deletions(-) create mode 100644 volatility3/framework/plugins/linux/hidden_modules.py diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 7bfa9268da..6e49e6f37d 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -344,3 +344,11 @@ def flags(self) -> str: # Boot time NSEC_PER_SEC = 1e9 + + +# Valid sizes for modules. Note that the Linux kernel does not define these values; they +# are based on empirical observations of typical memory allocations for kernel modules. +# We use this to verify that the found module falls within reasonable limits. +MODULE_MAXIMUM_CORE_SIZE = 20000000 +MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000 +MODULE_MINIMUM_SIZE = 4096 diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py new file mode 100644 index 0000000000..fd4b289430 --- /dev/null +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -0,0 +1,246 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List, Set, Tuple, Iterable +from volatility3.framework import renderers, interfaces, exceptions, objects +from volatility3.framework.constants import architectures +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import lsmod + +vollog = logging.getLogger(__name__) + + +class Hidden_modules(interfaces.plugins.PluginInterface): + """Carves memory to find hidden kernel modules""" + + _required_framework_version = (2, 10, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=architectures.LINUX_ARCHS, + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + ] + + @staticmethod + def get_modules_memory_boundaries( + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Tuple[int]: + """Determine the boundaries of the module allocation area + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A tuple containing the minimum and maximum addresses for the module allocation area. + """ + vmlinux = context.modules[vmlinux_module_name] + if vmlinux.has_symbol("mod_tree"): + # Kernel >= 5.19 58d208de3e8d87dbe196caf0b57cc58c7a3836ca + mod_tree = vmlinux.object_from_symbol("mod_tree") + modules_addr_min = mod_tree.addr_min + modules_addr_max = mod_tree.addr_max + elif vmlinux.has_symbol("module_addr_min"): + # 2.6.27 <= kernel < 5.19 3a642e99babe0617febb6f402e1e063479f489db + modules_addr_min = vmlinux.object_from_symbol("module_addr_min") + modules_addr_max = vmlinux.object_from_symbol("module_addr_max") + + if isinstance(modules_addr_min, objects.Void): + raise exceptions.VolatilityException( + "Your ISF symbols lack type information. You may need to update the" + "ISF using the latest version of dwarf2json" + ) + else: + raise exceptions.VolatilityException( + "Cannot find the module memory allocation area. Unsupported kernel" + ) + + return modules_addr_min, modules_addr_max + + @classmethod + def _get_module_address_alignment( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> int: + """Obtain the module memory address alignment. + + struct module is aligned to the L1 cache line, which is typically 64 bytes for most + common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this + will still work. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + The struct module alignment + """ + # FIXME: When dwarf2json/ISF supports type alignments. Read it directly from the type metadata + # Additionally, while 'context' and 'vmlinux_module_name' are currently unused, they will be + # essential for retrieving type metadata in the future. + return 64 + + @staticmethod + def _validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation + + Returns: + True if all the addresses meet the alignment + """ + return all(addr % address_alignment == 0 for addr in addresses) + + @classmethod + def get_hidden_modules( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Enumerate hidden modules by taking advantage of memory address alignment patterns + + This technique is much faster and uses less memory than the traditional scan method + in Volatility2, but it doesn't work with older kernels. + + From kernels 4.2 struct module allocation are aligned to the L1 cache line size. + In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in + the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can + also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json + doesn't support this feature yet. + In kernels < 4.2, alignment attributes are absent in the struct module, meaning + alignment cannot be guaranteed. Therefore, for older kernels, it's better to use + the traditional scan technique. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: + module objects + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + module_addr_min, module_addr_max = modules_memory_boundaries + module_address_alignment = cls._get_module_address_alignment( + context, vmlinux_module_name + ) + if not cls._validate_alignment_patterns( + known_module_addresses, module_address_alignment + ): + vollog.warning( + f"Module addresses aren't aligned to {module_address_alignment} bytes. " + "Switching to 1 byte aligment scan method." + ) + module_address_alignment = 1 + + mkobj_offset = vmlinux.get_type("module").relative_child_offset("mkobj") + mod_offset = vmlinux.get_type("module_kobject").relative_child_offset("mod") + offset_to_mkobj_mod = mkobj_offset + mod_offset + mod_member_template = vmlinux.get_type("module_kobject").child_template("mod") + mod_size = mod_member_template.size + mod_member_data_format = mod_member_template.data_format + + for module_addr in range( + module_addr_min, module_addr_max, module_address_alignment + ): + if module_addr in known_module_addresses: + continue + + try: + # This is just a pre-filter. Module readability and consistency are verified in module.is_valid() + self_referential_bytes = vmlinux_layer.read( + module_addr + offset_to_mkobj_mod, mod_size + ) + self_referential = objects.convert_data_to_value( + self_referential_bytes, int, mod_member_data_format + ) + if self_referential != module_addr: + continue + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): + continue + + module = vmlinux.object("module", offset=module_addr, absolute=True) + if module and module.is_valid(): + yield module + + @classmethod + def get_lsmod_module_addresses( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Set[int]: + """Obtain a set the known module addresses from linux.lsmod plugin + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A set containing known kernel module addresses + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + known_module_addresses = { + vmlinux_layer.canonicalize(module.vol.offset) + for module in lsmod.Lsmod.list_modules(context, vmlinux_module_name) + } + return known_module_addresses + + def _generator(self): + vmlinux_module_name = self.config["kernel"] + known_module_addresses = self.get_lsmod_module_addresses( + self.context, vmlinux_module_name + ) + modules_memory_boundaries = self.get_modules_memory_boundaries( + self.context, vmlinux_module_name + ) + for module in self.get_hidden_modules( + self.context, + vmlinux_module_name, + known_module_addresses, + modules_memory_boundaries, + ): + module_addr = module.vol.offset + module_name = module.get_name() or renderers.NotAvailableValue() + fields = (format_hints.Hex(module_addr), module_name) + yield (0, fields) + + def run(self): + if self.context.symbol_space.verify_table_versions( + "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) + ): + raise exceptions.SymbolSpaceError( + "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" + ) + + headers = [ + ("Address", format_hints.Hex), + ("Name", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 1f2812c106..3289775b6e 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -615,7 +615,7 @@ def _slot_to_nodep(self, slot) -> int: return nodep - def _iter_node(self, nodep, height) -> int: + def _iter_node(self, nodep, height) -> Iterator[int]: node = self.nodep_to_node(nodep) node_slots = node.slots for off in range(self.CHUNK_SIZE): @@ -632,7 +632,7 @@ def _iter_node(self, nodep, height) -> int: for child_node in self._iter_node(nodep, height - 1): yield child_node - def get_entries(self, root: interfaces.objects.ObjectInterface) -> int: + def get_entries(self, root: interfaces.objects.ObjectInterface) -> Iterator[int]: """Walks the tree data structure Args: @@ -818,7 +818,7 @@ def __init__( self._page_cache = page_cache self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name) - def get_cached_pages(self) -> interfaces.objects.ObjectInterface: + def get_cached_pages(self) -> Iterator[interfaces.objects.ObjectInterface]: """Returns all page cache contents Yields: diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index a9127907e0..0099b17e16 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -14,12 +14,7 @@ from volatility3.framework import constants, exceptions, objects, interfaces, symbols 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 -from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES -from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES, PT_FLAGS, NSEC_PER_SEC +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed @@ -33,112 +28,140 @@ class module(generic.GenericIntelProcess): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization + def is_valid(self): + """Determine whether it is a valid module object by verifying the self-referential + in module_kobject. This also confirms that the module is actively allocated and + not a remnant of freed memory or a failed module load attempt by verifying the + module memory section sizes. + """ + layer = self._context.layers[self.vol.layer_name] + # Make sure the entire module content is readable + if not layer.is_valid(self.vol.offset, self.vol.size): + return False + + core_size = self.get_core_size() + core_text_size = self.get_core_text_size() + init_size = self.get_init_size() + if not ( + 0 < core_text_size <= linux_constants.MODULE_MAXIMUM_CORE_TEXT_SIZE + and 0 < core_size <= linux_constants.MODULE_MAXIMUM_CORE_SIZE + and core_size + init_size >= linux_constants.MODULE_MINIMUM_SIZE + ): + return False - @property - def mod_mem_type(self): + if not ( + self.mkobj + and self.mkobj.mod + and self.mkobj.mod.is_readable() + and self.mkobj.mod == self.vol.offset + ): + return False + + return True + + @functools.cached_property + def mod_mem_type(self) -> Dict: """Return the mod_mem_type enum choices if available or an empty dict if not""" # mod_mem_type and module_memory were added in kernel 6.4 which replaces # module_layout for storing the information around core_layout etc. # see commit ac3b43283923440900b4f36ca5f9f0b1ca43b70e for more information + symbol_table_name = self.get_symbol_table_name() + mod_mem_type_symname = symbol_table_name + constants.BANG + "mod_mem_type" + symbol_space = self._context.symbol_space + try: + mod_mem_type = symbol_space.get_enumeration(mod_mem_type_symname).choices + except exceptions.SymbolError: + mod_mem_type = {} + vollog.debug( + "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" + ) - if self._mod_mem_type is None: - try: - self._mod_mem_type = self._context.symbol_space.get_enumeration( - self.get_symbol_table_name() + constants.BANG + "mod_mem_type" - ).choices - except exceptions.SymbolError: - vollog.debug( - "Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4" - ) - # set to empty dict to show that the enum was not found, and so shouldn't be searched for again - self._mod_mem_type = {} - return self._mod_mem_type + return mod_mem_type + + def _get_mem_type(self, mod_mem_type_name): + module_mem_index = self.mod_mem_type.get(mod_mem_type_name) + if module_mem_index is None: + raise AttributeError(f"Unknown module memory type '{mod_mem_type_name}'") + + if not (0 <= module_mem_index < self.mem.count): + raise AttributeError( + f"Invalid module memory type index '{module_mem_index}'" + ) + + return self.mem[module_mem_index] + + def _get_mem_size(self, mod_mem_type_name): + return self._get_mem_type(mod_mem_type_name).size + + def _get_mem_base(self, mod_mem_type_name): + return self._get_mem_type(mod_mem_type_name).base def get_module_base(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_base: Unable to get module base. Cannot read base from MOD_TEXT." - ) + return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): return self.core_layout.base elif self.has_member("module_core"): return self.module_core - raise AttributeError("module -> get_module_base: Unable to get module base") + + raise AttributeError("Unable to get module base") def get_init_size(self): if self.has_member("mem"): # kernels 6.4+ - try: - return ( - self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].size - + self.mem[self.mod_mem_type["MOD_INIT_DATA"]].size - + self.mem[self.mod_mem_type["MOD_INIT_RODATA"]].size - ) - except KeyError: - raise AttributeError( - "module -> get_init_size: Unable to determine .init section size of module. Cannot read size of MOD_INIT_TEXT, MOD_INIT_DATA, and MOD_INIT_RODATA" - ) + return ( + self._get_mem_size("MOD_INIT_TEXT") + + self._get_mem_size("MOD_INIT_DATA") + + self._get_mem_size("MOD_INIT_RODATA") + ) elif self.has_member("init_layout"): return self.init_layout.size elif self.has_member("init_size"): return self.init_size - raise AttributeError( - "module -> get_init_size: Unable to determine .init section size of module" - ) + + raise AttributeError("Unable to determine .init section size of module") def get_core_size(self): if self.has_member("mem"): # kernels 6.4+ - try: - return ( - self.mem[self.mod_mem_type["MOD_TEXT"]].size - + self.mem[self.mod_mem_type["MOD_DATA"]].size - + self.mem[self.mod_mem_type["MOD_RODATA"]].size - + self.mem[self.mod_mem_type["MOD_RO_AFTER_INIT"]].size - ) - except KeyError: - raise AttributeError( - "module -> get_core_size: Unable to determine core size of module. Cannot read size of MOD_TEXT, MOD_DATA, MOD_RODATA, and MOD_RO_AFTER_INIT." - ) + return ( + self._get_mem_size("MOD_TEXT") + + self._get_mem_size("MOD_DATA") + + self._get_mem_size("MOD_RODATA") + + self._get_mem_size("MOD_RO_AFTER_INIT") + ) elif self.has_member("core_layout"): return self.core_layout.size elif self.has_member("core_size"): return self.core_size - raise AttributeError( - "module -> get_core_size: Unable to determine core size of module" - ) + + raise AttributeError("Unable to determine core size of module") + + def get_core_text_size(self): + if self.has_member("mem"): # kernels 6.4+ + return self._get_mem_size("MOD_TEXT") + elif self.has_member("core_layout"): + return self.core_layout.text_size + elif self.has_member("core_text_size"): + return self.core_text_size + + raise AttributeError("Unable to determine core text size of module") def get_module_core(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_core: Unable to get module core. Cannot read base from MOD_TEXT." - ) + return self._get_mem_base("MOD_TEXT") elif self.has_member("core_layout"): return self.core_layout.base elif self.has_member("module_core"): return self.module_core - raise AttributeError("module -> get_module_core: Unable to get module core") + raise AttributeError("Unable to get module core") def get_module_init(self): if self.has_member("mem"): # kernels 6.4+ - try: - return self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].base - except KeyError: - raise AttributeError( - "module -> get_module_core: Unable to get module init. Cannot read base from MOD_INIT_TEXT." - ) + return self._get_mem_base("MOD_INIT_TEXT") elif self.has_member("init_layout"): return self.init_layout.base elif self.has_member("module_init"): return self.module_init - raise AttributeError("module -> get_module_init: Unable to get module init") + raise AttributeError("Unable to get module init") def get_name(self): """Get the name of the module as a string""" @@ -340,7 +363,7 @@ def is_kernel_thread(self) -> bool: Returns: bool: True, if this task is a kernel thread. Otherwise, False. """ - return (self.flags & constants.linux.PF_KTHREAD) != 0 + return (self.flags & linux_constants.PF_KTHREAD) != 0 @property def is_thread_group_leader(self) -> bool: @@ -417,7 +440,11 @@ def get_ptrace_tracee_tids(self) -> List[int]: def get_ptrace_tracee_flags(self) -> Optional[str]: """Returns a string with the ptrace flags""" - return PT_FLAGS(self.ptrace).flags if self.is_being_ptraced else None + return ( + linux_constants.PT_FLAGS(self.ptrace).flags + if self.is_being_ptraced + else None + ) def _get_task_start_time(self) -> datetime.timedelta: """Returns the task's monotonic start_time as a timedelta. @@ -1492,7 +1519,7 @@ def is_equal(self, vfsmount_ptr) -> bool: bool: 'True' if the given argument points to the the same 'vfsmount' as 'self'. """ - if type(vfsmount_ptr) == objects.Pointer: + if isinstance(vfsmount_ptr, objects.Pointer): return self.vol.offset == vfsmount_ptr else: raise exceptions.VolatilityException( @@ -1715,18 +1742,18 @@ def get_inode(self): def get_state(self): socket_state_idx = self.state - if 0 <= socket_state_idx < len(SOCKET_STATES): - return SOCKET_STATES[socket_state_idx] + if 0 <= socket_state_idx < len(linux_constants.SOCKET_STATES): + return linux_constants.SOCKET_STATES[socket_state_idx] class sock(objects.StructType): def get_family(self): family_idx = self.__sk_common.skc_family - if 0 <= family_idx < len(SOCK_FAMILY): - return SOCK_FAMILY[family_idx] + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] def get_type(self): - return SOCK_TYPES.get(self.sk_type, "") + return linux_constants.SOCK_TYPES.get(self.sk_type, "") def get_inode(self): if not self.sk_socket: @@ -1760,8 +1787,8 @@ def get_state(self): # Unix socket states reuse (a subset) of the inet_sock states contants if self.sk.get_type() == "STREAM": state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(TCP_STATES): - return TCP_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] else: # Return the generic socket state return self.sk.sk_socket.get_state() @@ -1773,15 +1800,15 @@ def get_inode(self): class inet_sock(objects.StructType): def get_family(self): family_idx = self.sk.__sk_common.skc_family - if 0 <= family_idx < len(SOCK_FAMILY): - return SOCK_FAMILY[family_idx] + if 0 <= family_idx < len(linux_constants.SOCK_FAMILY): + return linux_constants.SOCK_FAMILY[family_idx] def get_protocol(self): # If INET6 family and a proto is defined, we use that specific IPv6 protocol. # Otherwise, we use the standard IP protocol. - protocol = IP_PROTOCOLS.get(self.sk.sk_protocol) + protocol = linux_constants.IP_PROTOCOLS.get(self.sk.sk_protocol) if self.get_family() == "AF_INET6": - protocol = IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) + protocol = linux_constants.IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) return protocol def get_state(self): @@ -1789,8 +1816,8 @@ def get_state(self): if self.sk.get_type() == "STREAM": state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(TCP_STATES): - return TCP_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.TCP_STATES): + return linux_constants.TCP_STATES[state_idx] else: # Return the generic socket state return self.sk.sk_socket.get_state() @@ -1873,8 +1900,8 @@ def get_dst_addr(self): class netlink_sock(objects.StructType): def get_protocol(self): protocol_idx = self.sk.sk_protocol - if 0 <= protocol_idx < len(NETLINK_PROTOCOLS): - return NETLINK_PROTOCOLS[protocol_idx] + if 0 <= protocol_idx < len(linux_constants.NETLINK_PROTOCOLS): + return linux_constants.NETLINK_PROTOCOLS[protocol_idx] def get_state(self): # Return the generic socket state @@ -1916,8 +1943,8 @@ def get_protocol(self): eth_proto = socket_module.htons(self.num) if eth_proto == 0: return None - elif eth_proto in ETH_PROTOCOLS: - return ETH_PROTOCOLS[eth_proto] + elif eth_proto in linux_constants.ETH_PROTOCOLS: + return linux_constants.ETH_PROTOCOLS[eth_proto] else: return f"0x{eth_proto:x}" @@ -1929,13 +1956,13 @@ def get_state(self): class bt_sock(objects.StructType): def get_protocol(self): type_idx = self.sk.sk_protocol - if 0 <= type_idx < len(BLUETOOTH_PROTOCOLS): - return BLUETOOTH_PROTOCOLS[type_idx] + if 0 <= type_idx < len(linux_constants.BLUETOOTH_PROTOCOLS): + return linux_constants.BLUETOOTH_PROTOCOLS[type_idx] def get_state(self): state_idx = self.sk.__sk_common.skc_state - if 0 <= state_idx < len(BLUETOOTH_STATES): - return BLUETOOTH_STATES[state_idx] + if 0 <= state_idx < len(linux_constants.BLUETOOTH_STATES): + return linux_constants.BLUETOOTH_STATES[state_idx] class xdp_sock(objects.StructType): @@ -2053,7 +2080,7 @@ def get_last_cap_value(cls) -> int: Returns: int: The latest capability ID supported by the framework. """ - return len(CAPABILITIES) - 1 + return len(linux_constants.CAPABILITIES) - 1 def get_kernel_cap_full(self) -> int: """Return the maximum value allowed for this kernel for a capability @@ -2082,7 +2109,7 @@ def capabilities_to_string(cls, capabilities_bitfield: int) -> List[str]: """ capabilities = [] - for bit, name in enumerate(CAPABILITIES): + for bit, name in enumerate(linux_constants.CAPABILITIES): if capabilities_bitfield & (1 << bit) != 0: capabilities.append(name) @@ -2143,10 +2170,10 @@ def has_capability(self, capability: str) -> bool: Returns: bool: "True" if the given capability is enabled. """ - if capability not in CAPABILITIES: + if capability not in linux_constants.CAPABILITIES: raise AttributeError(f"Unknown capability with name '{capability}'") - cap_value = 1 << CAPABILITIES.index(capability) + cap_value = 1 << linux_constants.CAPABILITIES.index(capability) return cap_value & self.get_capabilities() != 0 From c0fa2cfcd67d676dab100150daad2f93948c641b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 5 Nov 2024 20:02:55 +1100 Subject: [PATCH 230/250] Linux: Boottime API: User linux_constanst import --- .../symbols/linux/extensions/__init__.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0099b17e16..d05c34304a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2217,12 +2217,12 @@ def new_from_nsec(cls, nsec) -> "Timespec64Concrete": # Based on ns_to_timespec64() if nsec > 0: - tv_sec = nsec // NSEC_PER_SEC - tv_nsec = nsec % NSEC_PER_SEC + tv_sec = nsec // linux_constants.NSEC_PER_SEC + tv_nsec = nsec % linux_constants.NSEC_PER_SEC elif nsec < 0: - tv_sec = -((-nsec - 1) // NSEC_PER_SEC) - 1 - rem = (-nsec - 1) % NSEC_PER_SEC - tv_nsec = NSEC_PER_SEC - rem - 1 + tv_sec = -((-nsec - 1) // linux_constants.NSEC_PER_SEC) - 1 + rem = (-nsec - 1) % linux_constants.NSEC_PER_SEC + tv_nsec = linux_constants.NSEC_PER_SEC - rem - 1 else: tv_sec = tv_nsec = 0 @@ -2233,13 +2233,15 @@ def to_datetime(self) -> datetime.datetime: # pylint: disable=E1101 return conversion.unixtime_to_datetime( - self.tv_sec + self.tv_nsec / NSEC_PER_SEC + self.tv_sec + self.tv_nsec / linux_constants.NSEC_PER_SEC ) def to_timedelta(self) -> datetime.timedelta: """Converts this Timespec64Abstract subclass object to timedelta""" # pylint: disable=E1101 - return datetime.timedelta(seconds=self.tv_sec + self.tv_nsec / NSEC_PER_SEC) + return datetime.timedelta( + seconds=self.tv_sec + self.tv_nsec / linux_constants.NSEC_PER_SEC + ) def __add__(self, other) -> "Timespec64Concrete": """Returns a new Timespec64Concrete object that sums the current values with those @@ -2282,12 +2284,12 @@ def normalize(self): # Based on kernel's set_normalized_timespec64() # pylint: disable=E1101 - while self.tv_nsec >= NSEC_PER_SEC: - self.tv_nsec -= NSEC_PER_SEC + while self.tv_nsec >= linux_constants.NSEC_PER_SEC: + self.tv_nsec -= linux_constants.NSEC_PER_SEC self.tv_sec += 1 while self.tv_nsec < 0: - self.tv_nsec += NSEC_PER_SEC + self.tv_nsec += linux_constants.NSEC_PER_SEC self.tv_sec -= 1 From a5253ac2767ca3e0051dcc3a78de7d9b6a7589b8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 7 Nov 2024 13:40:18 +0100 Subject: [PATCH 231/250] re-trigger black --- volatility3/framework/plugins/windows/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index 5ce2279ccb..1df090b5b4 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -140,7 +140,7 @@ def list_injections( dirty_page_check = True break except exceptions.InvalidAddressException: - # Abort as it is likely that other addresses in the same range will also fail + # Abort as it is likely that other addresses in the same range will also fail. break if not dirty_page_check: continue From 20317e0d13d049070201562a1617ea89256879dc Mon Sep 17 00:00:00 2001 From: Joren Vrancken Date: Thu, 7 Nov 2024 18:05:36 +0100 Subject: [PATCH 232/250] Improve windows.amcache plugin description --- volatility3/framework/plugins/windows/amcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index 8d0a4769ea..1e918d61c5 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -215,7 +215,7 @@ def _get_datetime_str_value( class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): - """Scans for windows services.""" + """Extract information on executed applications from the AmCache.""" _required_framework_version = (2, 0, 0) _version = (1, 0, 0) From 6494260d916838c8f1a854450e217049dff015bb Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 7 Nov 2024 23:33:52 +0000 Subject: [PATCH 233/250] Core: Limit capstone to compatible versions --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index bd61edf142..e0d3663911 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,8 @@ yara-python>=3.8.0 # This is required for several plugins that perform malware analysis and disassemble code. # It can also improve accuracy of Windows 8 and later memory samples. -capstone>=3.0.5 +# FIXME: Version 6.0.0 is incompatible (#1336) so we'll need an adaptor at some point +capstone>=3.0.5,<6.0.0 # This is required by plugins that decrypt passwords, password hashes, etc. pycryptodome @@ -19,4 +20,4 @@ leechcorepyc>=2.4.0; sys_platform != 'darwin' # This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage gcsfs>=2023.1.0 -s3fs>=2023.1.0 \ No newline at end of file +s3fs>=2023.1.0 From 74cf394ad0adb65d9cc149dae0451d7bee3d76a8 Mon Sep 17 00:00:00 2001 From: Joren Vrancken Date: Fri, 8 Nov 2024 10:06:59 +0100 Subject: [PATCH 234/250] Print plugin description on plugin --help --- volatility3/cli/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index b4e993dacc..75b62abf6a 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -367,7 +367,9 @@ def run(self): ) for plugin in sorted(plugin_list): plugin_parser = subparser.add_parser( - plugin, help=plugin_list[plugin].__doc__ + plugin, + help=plugin_list[plugin].__doc__, + description=plugin_list[plugin].__doc__, ) self.populate_requirements_argparse(plugin_parser, plugin_list[plugin]) From b37ba26dd62a9cc85c89e174c20f1afe549eb753 Mon Sep 17 00:00:00 2001 From: Joren Vrancken Date: Fri, 8 Nov 2024 10:19:05 +0100 Subject: [PATCH 235/250] Fix typo in SuspiciousThreads --- volatility3/framework/plugins/windows/suspicious_threads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index 2679b191ac..4bfb6baa54 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -13,11 +13,11 @@ vollog = logging.getLogger(__name__) -class SupsiciousThreads(interfaces.plugins.PluginInterface): +class SuspiciousThreads(interfaces.plugins.PluginInterface): """Lists suspicious userland process threads""" _required_framework_version = (2, 4, 0) - _version = (2, 0, 0) + _version = (2, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 106b3b3dd0849194c4b83b9e254365b0c8f6070c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 16:01:04 +1100 Subject: [PATCH 236/250] Linux: PageCache: Fix inode offset when logging an error --- volatility3/framework/plugins/linux/pagecache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 81e1f36015..005fc9acc0 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -454,7 +454,7 @@ def write_inode_content_to_file( if current_fp + len(page_bytes) > inode_size: vollog.error( "Page out of file bounds: inode 0x%x, inode size %d, page index %d", - inode.vol.object, + inode.vol.offset, inode_size, page_idx, ) From 3216700d8b6b0488f4a7431d6c1d940fc5299f24 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 16:05:59 +1100 Subject: [PATCH 237/250] Linux: module object extension: Fix recursion call issue --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d05c34304a..bc8e25105e 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -294,7 +294,7 @@ def num_symtab(self): if self.has_member("kallsyms"): return int(self.kallsyms.num_symtab) elif self.has_member("num_symtab"): - return int(self.num_symtab) + return int(self.member("num_symtab")) raise AttributeError( "module -> num_symtab: Unable to determine number of symbols" ) From a2e99b2633a0099e19bbb5692d7919a9e5cd17cd Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 16:13:50 +1100 Subject: [PATCH 238/250] Linux: Object extensions: Fix type annotations for improved clarity and accuracy. --- .../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 bc8e25105e..41d764f949 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -240,7 +240,7 @@ def get_symbols(self): for sym in syms: yield sym - def get_symbols_names_and_addresses(self) -> Tuple[str, int]: + def get_symbols_names_and_addresses(self) -> Iterable[Tuple[str, int]]: """Get names and addresses for each symbol of the module Yields: @@ -1098,7 +1098,7 @@ def d_ancestor(self, ancestor_dentry): current_dentry = current_dentry.d_parent return None - def get_subdirs(self) -> interfaces.objects.ObjectInterface: + def get_subdirs(self) -> Iterable[interfaces.objects.ObjectInterface]: """Walks dentry subdirs Yields: @@ -2435,7 +2435,7 @@ def get_file_mode(self) -> str: """ return stat.filemode(self.i_mode) - def get_pages(self) -> interfaces.objects.ObjectInterface: + def get_pages(self) -> Iterable[interfaces.objects.ObjectInterface]: """Gets the inode's cached pages Yields: @@ -2643,7 +2643,7 @@ def idr_find(self, idr_id: int) -> int: return idr_layer - def _old_kernel_get_entries(self) -> int: + def _old_kernel_get_entries(self) -> Iterable[int]: # Kernels < 4.11 cur = self.cur total = next_id = 0 @@ -2655,7 +2655,7 @@ def _old_kernel_get_entries(self) -> int: next_id += 1 - def _new_kernel_get_entries(self) -> int: + def _new_kernel_get_entries(self) -> Iterable[int]: # Kernels >= 4.11 id_storage = linux.IDStorage.choose_id_storage( self._context, kernel_module_name="kernel" @@ -2663,7 +2663,7 @@ def _new_kernel_get_entries(self) -> int: for page_addr in id_storage.get_entries(root=self.idr_rt): yield page_addr - def get_entries(self) -> int: + def get_entries(self) -> Iterable[int]: """Walks the IDR and yield a pointer associated with each element. Args: From 3a734fdeb159c8ff31b216b5ef1249d1dd4d3df7 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 16:23:33 +1100 Subject: [PATCH 239/250] Linux: Object extensions: Remove redundant log message header; the logger handles this formatting. If needed, we can adjust the logger's format instead --- .../framework/symbols/linux/extensions/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 41d764f949..927f767e2a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -287,7 +287,7 @@ def section_symtab(self): return self.kallsyms.symtab elif self.has_member("symtab"): return self.symtab - raise AttributeError("module -> symtab: Unable to get symtab") + raise AttributeError("Unable to get symtab") @property def num_symtab(self): @@ -295,9 +295,7 @@ def num_symtab(self): return int(self.kallsyms.num_symtab) elif self.has_member("num_symtab"): return int(self.member("num_symtab")) - raise AttributeError( - "module -> num_symtab: Unable to determine number of symbols" - ) + raise AttributeError("Unable to determine number of symbols") @property def section_strtab(self): @@ -307,7 +305,7 @@ def section_strtab(self): # Older kernels elif self.has_member("strtab"): return self.strtab - raise AttributeError("module -> strtab: Unable to get strtab") + raise AttributeError("Unable to get strtab") class task_struct(generic.GenericIntelProcess): From d90019657c26b2f6f723a5225ad99ad6c3fd012b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 18:14:36 +1100 Subject: [PATCH 240/250] Linux: Add kthreads plugin to enumerate kernel thread functions --- .../framework/plugins/linux/kthreads.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 volatility3/framework/plugins/linux/kthreads.py diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py new file mode 100644 index 0000000000..639759e490 --- /dev/null +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -0,0 +1,109 @@ +# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import logging +from typing import List + +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux +from volatility3.framework.constants import architectures +from volatility3.framework.objects import utility +from volatility3.plugins.linux import pslist, lsmod + +vollog = logging.getLogger(__name__) + + +class Kthreads(plugins.PluginInterface): + """Enumerates kthread functions""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) + handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, vmlinux.name, modules + ) + + kthread_type = vmlinux.get_type( + vmlinux.symbol_table_name + constants.BANG + "kthread" + ) + + if not kthread_type.has_member("threadfn"): + raise exceptions.VolatilityException( + "Unsupported kthread implementation. This plugin only works with kernels >= 5.8" + ) + + for task in pslist.PsList.list_tasks( + self.context, vmlinux.name, include_threads=True + ): + if not task.is_kernel_thread: + continue + + if task.has_member("worker_private"): + # kernels >= 5.17 e32cf5dfbe227b355776948b2c9b5691b84d1cbd + ktread_base_pointer = task.worker_private + else: + # 5.8 <= kernels < 5.17 in 52782c92ac85c4e393eb4a903a62e6c24afa633f threadfn + # was added to struct kthread. task.set_child_tid is safe on those versions. + ktread_base_pointer = task.set_child_tid + + if not ktread_base_pointer.is_readable(): + continue + + kthread = ktread_base_pointer.dereference().cast("kthread") + threadfn = kthread.threadfn + if not (threadfn and threadfn.is_readable()): + continue + + task_name = utility.array_to_string(task.comm) + + # kernels >= 5.17 in d6986ce24fc00b0638bd29efe8fb7ba7619ed2aa full_name was added to kthread + thread_name = ( + utility.pointer_to_string(kthread.full_name, count=255) + if kthread.has_member("full_name") + else task_name + ) + module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( + vmlinux, handlers, threadfn + ) + + fields = [ + task.pid, + thread_name, + format_hints.Hex(threadfn), + module_name, + symbol_name, + ] + yield 0, fields + + def run(self): + return renderers.TreeGrid( + [ + ("TID", int), + ("Thread Name", str), + ("Handler Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) From 780f9ab6f569af2c4e0095cb529736c2d5ef992d Mon Sep 17 00:00:00 2001 From: eve Date: Tue, 12 Nov 2024 07:22:45 +0000 Subject: [PATCH 241/250] Volshell: add regex_scan --- volatility3/cli/volshell/generic.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index a0477656e2..9936244ee0 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -14,7 +14,7 @@ from volatility3.cli import text_renderer, volshell from volatility3.framework import exceptions, interfaces, objects, plugins, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.layers import intel, physical, resources +from volatility3.framework.layers import intel, physical, resources, scanners try: import capstone @@ -149,6 +149,7 @@ def construct_locals(self) -> List[Tuple[List[str], Any]]: (["cc", "create_configurable"], self.create_configurable), (["lf", "load_file"], self.load_file), (["rs", "run_script"], self.run_script), + (["re", "regex_scan"], self.regex_scan), ] def _construct_locals_dict(self) -> Dict[str, Any]: @@ -288,6 +289,21 @@ def display_words(self, offset, count=128, layer_name=None): remaining_data = self._read_data(offset, count=count, layer_name=layer_name) self._display_data(offset, remaining_data, format_string="H") + def regex_scan(self, pattern, count=128, layer_name=None): + """Scans for regex pattern in layer using RegExScanner.""" + if not isinstance(pattern, bytes): + raise TypeError("pattern must be bytes, e.g. re(b'pattern')") + layer_name_to_scan = layer_name or self.current_layer + for offset in self.context.layers[layer_name_to_scan].scan( + scanner=scanners.RegExScanner(pattern), + context=self.context, + ): + remaining_data = self._read_data( + offset, count=count, layer_name=layer_name_to_scan + ) + self._display_data(offset, remaining_data) + print("") + def disassemble(self, offset, count=128, layer_name=None, architecture=None): """Disassembles a number of instructions from the code at offset""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) From 8efe429719f50603e35a170b3eb37e39fc43f5fc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 12 Nov 2024 18:32:26 +1100 Subject: [PATCH 242/250] Linux: kthreads plugin: Adjust required framework version --- volatility3/framework/plugins/linux/kthreads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 639759e490..2637934956 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -19,7 +19,7 @@ class Kthreads(plugins.PluginInterface): """Enumerates kthread functions""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 11, 0) _version = (1, 0, 0) From 787e15ac2b51987db496f38b6d9fa17e4eb43fe9 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Wed, 13 Nov 2024 09:50:08 +1100 Subject: [PATCH 243/250] Linux: kthreads plugin: Add missing requirements --- volatility3/framework/plugins/linux/kthreads.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/volatility3/framework/plugins/linux/kthreads.py b/volatility3/framework/plugins/linux/kthreads.py index 2637934956..2e51b4688c 100644 --- a/volatility3/framework/plugins/linux/kthreads.py +++ b/volatility3/framework/plugins/linux/kthreads.py @@ -34,6 +34,12 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.VersionRequirement( name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 3, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self): From af7a420cdc14689ee6d2ff72d5ac8eefad9e833d Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 13 Nov 2024 08:56:58 +0000 Subject: [PATCH 244/250] Volshell: rename regex_scan short hand to rx so that it does not clash with the built in python re module --- volatility3/cli/volshell/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 9936244ee0..b196360d52 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -149,7 +149,7 @@ def construct_locals(self) -> List[Tuple[List[str], Any]]: (["cc", "create_configurable"], self.create_configurable), (["lf", "load_file"], self.load_file), (["rs", "run_script"], self.run_script), - (["re", "regex_scan"], self.regex_scan), + (["rx", "regex_scan"], self.regex_scan), ] def _construct_locals_dict(self) -> Dict[str, Any]: @@ -292,7 +292,7 @@ def display_words(self, offset, count=128, layer_name=None): def regex_scan(self, pattern, count=128, layer_name=None): """Scans for regex pattern in layer using RegExScanner.""" if not isinstance(pattern, bytes): - raise TypeError("pattern must be bytes, e.g. re(b'pattern')") + raise TypeError("pattern must be bytes, e.g. rx(b'pattern')") layer_name_to_scan = layer_name or self.current_layer for offset in self.context.layers[layer_name_to_scan].scan( scanner=scanners.RegExScanner(pattern), From 357d8a914b4a69d231b4ed9de3aecdcf7179be7f Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 14 Nov 2024 10:55:41 +0000 Subject: [PATCH 245/250] Volshell: Make the default number of bytes returned by db, dw, dd, dq, rx, and dis use the constant DEFAULT_NUM_DISPLAY_BYTES --- volatility3/cli/volshell/generic.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index b196360d52..82c470e1af 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -29,6 +29,8 @@ class Volshell(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) + DEFAULT_NUM_DISPLAY_BYTES = 128 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__current_layer: Optional[str] = None @@ -269,27 +271,31 @@ def change_kernel(self, kernel_name: str = None): self.__current_kernel_name = kernel_name print(f"Current kernel : {self.current_kernel_name}") - def display_bytes(self, offset, count=128, layer_name=None): + def display_bytes(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None): """Displays byte values and ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) self._display_data(offset, remaining_data) - def display_quadwords(self, offset, count=128, layer_name=None): + def display_quadwords( + self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None + ): """Displays quad-word values (8 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) self._display_data(offset, remaining_data, format_string="Q") - def display_doublewords(self, offset, count=128, layer_name=None): + def display_doublewords( + self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None + ): """Displays double-word values (4 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) self._display_data(offset, remaining_data, format_string="I") - def display_words(self, offset, count=128, layer_name=None): + def display_words(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None): """Displays word values (2 bytes) and corresponding ASCII characters""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) self._display_data(offset, remaining_data, format_string="H") - def regex_scan(self, pattern, count=128, layer_name=None): + def regex_scan(self, pattern, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None): """Scans for regex pattern in layer using RegExScanner.""" if not isinstance(pattern, bytes): raise TypeError("pattern must be bytes, e.g. rx(b'pattern')") @@ -304,7 +310,13 @@ def regex_scan(self, pattern, count=128, layer_name=None): self._display_data(offset, remaining_data) print("") - def disassemble(self, offset, count=128, layer_name=None, architecture=None): + def disassemble( + self, + offset, + count=DEFAULT_NUM_DISPLAY_BYTES, + layer_name=None, + architecture=None, + ): """Disassembles a number of instructions from the code at offset""" remaining_data = self._read_data(offset, count=count, layer_name=layer_name) if not has_capstone: From bee3f001b66fa6a9ba9b1e325cb2fa2e273f8ff2 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 15 Nov 2024 11:31:30 +1100 Subject: [PATCH 246/250] linux: netfilter plugin: Enhance docstring to provide a clearer explanation of 'hooked' --- volatility3/framework/plugins/linux/netfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 66075f907d..b371d5ad21 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -174,7 +174,7 @@ def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: priority [int]: Priority hook_ops_hook [int]: Hook address module_name [str]: Linux kernel module name - hooked [bool]: hooked? + hooked [bool]: "True" if the network stack has been hijacked """ for netns, net in self.get_net_namespaces(): for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop(): From c909fdd4e76af6f8743a5df9b2ab38c981e68003 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 15 Nov 2024 11:53:05 +1100 Subject: [PATCH 247/250] linux: netfilter plugin: Bump minor version --- volatility3/framework/plugins/linux/netfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index b371d5ad21..73496dfd96 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -675,7 +675,7 @@ class Netfilter(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) _required_linuxutils_version = (2, 1, 0) _required_lsmod_version = (2, 0, 0) From 1ba5b04b7d3ef1b9a8911eae973da10f4d33d7e5 Mon Sep 17 00:00:00 2001 From: eve Date: Mon, 18 Nov 2024 21:20:04 +0000 Subject: [PATCH 248/250] Windows: Remove dep on vadyarascan from svcscan plugin --- volatility3/framework/plugins/windows/svcscan.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 52ed5e759e..ca390561f7 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -20,7 +20,7 @@ from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import versions from volatility3.framework.symbols.windows.extensions import services as services_types -from volatility3.plugins.windows import poolscanner, pslist, vadyarascan +from volatility3.plugins.windows import poolscanner, pslist from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class SvcScan(interfaces.plugins.PluginInterface): """Scans for windows services.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 0) + _version = (3, 0, 1) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -60,9 +60,6 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) ), - requirements.PluginRequirement( - name="vadyarascan", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) - ), requirements.PluginRequirement( name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) ), @@ -317,10 +314,17 @@ def service_scan( layer = context.layers[proc_layer_name] + # get process sections for scanning + sections = [] + for vad in task.get_vad_root().traverse(): + base = vad.get_start() + if vad.get_size(): + sections.append((base, vad.get_size())) + for offset in layer.scan( context=context, scanner=scanners.BytesScanner(needle=service_tag), - sections=vadyarascan.VadYaraScan.get_vad_maps(task), + sections=sections, ): if not is_vista_or_later: service_record = context.object( From 104f430b2c49cb5887ab90d5574eb515f5d1b8a8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 21 Dec 2024 20:04:02 +0000 Subject: [PATCH 249/250] Plugins: Update vmayarascan and vadyarascan --- .../framework/plugins/linux/vmayarascan.py | 61 ++++++++++------- .../framework/plugins/windows/vadyarascan.py | 68 +++++++++---------- 2 files changed, 69 insertions(+), 60 deletions(-) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 9fe06b0c84..9f6a7a9b88 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging from typing import Iterable, List, Tuple from volatility3.framework import interfaces, renderers @@ -10,12 +11,14 @@ from volatility3.plugins import yarascan from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class VmaYaraScan(interfaces.plugins.PluginInterface): """Scans all virtual memory areas for tasks using yara.""" _required_framework_version = (2, 4, 0) - _version = (1, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -33,6 +36,9 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), requirements.ModuleRequirement( name="kernel", description="Linux kernel", @@ -50,6 +56,8 @@ def _generator(self): # use yarascan to parse the yara options provided and create the rules rules = yarascan.YaraScan.process_yara_options(dict(self.config)) + sanity_check = 1024 * 1024 * 1024 # 1 GB + # filter based on the pid option if provided filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for task in pslist.PsList.list_tasks( @@ -66,29 +74,36 @@ def _generator(self): # get the proc_layer object from the context proc_layer = self.context.layers[proc_layer_name] - for start, end in self.get_vma_maps(task): - for match in rules.match( - data=proc_layer.read(start, end - start, True) + max_vma_size = 0 + vma_maps_to_scan = [] + for start, size in self.get_vma_maps(task): + if size > sanity_check: + vollog.debug( + f"VMA at 0x{start:x} over sanity-check size, not scanning" + ) + continue + max_vma_size = max(max_vma_size, size) + vma_maps_to_scan.append((start, size)) + + if not vma_maps_to_scan: + vollog.warning(f"No VMAs were found for task {task.tgid}, not scanning") + continue + + scanner = yarascan.YaraScanner(rules=rules) + scanner.chunk_size = max_vma_size + + # scan the VMA data (in one contiguous block) with the yarascanner + for start, size in vma_maps_to_scan: + for offset, rule_name, name, value in scanner( + proc_layer.read(start, size, pad=True), start ): - if yarascan.YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield 0, ( - format_hints.Hex(offset + start), - task.tgid, - match.rule, - name, - value, - ) + yield 0, ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + value, + ) @staticmethod def get_vma_maps( diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index efcc70d07f..2e9cc44ea0 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -32,6 +32,9 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] requirements.PluginRequirement( name="pslist", plugin=pslist.PsList, version=(2, 0, 0) ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), requirements.PluginRequirement( name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0) ), @@ -66,49 +69,40 @@ def _generator(self): ): layer_name = task.add_process_layer() layer = self.context.layers[layer_name] + + max_vad_size = 0 + vad_maps_to_scan = [] + for start, size in self.get_vad_maps(task): if size > sanity_check: vollog.debug( f"VAD at 0x{start:x} over sanity-check size, not scanning" ) continue - - data = layer.read(start, size, True) - if not yarascan.YaraScan._yara_x: - for match in rules.match(data=data): - if yarascan.YaraScan.yara_returns_instances(): - for match_string in match.strings: - for instance in match_string.instances: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - match.rule, - match_string.identifier, - instance.matched_data, - ) - else: - for offset, name, value in match.strings: - yield 0, ( - format_hints.Hex(offset + start), - task.UniqueProcessId, - match.rule, - name, - value, - ) - else: - for match in rules.scan(data).matching_rules: - for match_string in match.patterns: - for instance in match_string.matches: - yield 0, ( - format_hints.Hex(instance.offset + start), - task.UniqueProcessId, - f"{match.namespace}.{match.identifier}", - match_string.identifier, - data[ - instance.offset : instance.offset - + instance.length - ], - ) + max_vad_size = max(max_vad_size, size) + vad_maps_to_scan.append((start, size)) + + if not vad_maps_to_scan: + vollog.warning( + f"No VADs were found for task {task.UniqueProcessID}, not scanning" + ) + continue + + scanner = yarascan.YaraScanner(rules=rules) + scanner.chunk_size = max_vad_size + + # scan the VAD data (in one contiguous block) with the yarascanner + for start, size in vad_maps_to_scan: + for offset, rule_name, name, value in scanner( + layer.read(start, size, pad=True), start + ): + yield 0, ( + format_hints.Hex(offset), + task.UniqueProcessId, + rule_name, + name, + value, + ) @staticmethod def get_vad_maps( From 3ab5fa90424b86904bca0a4ca987370aa890249f Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 16 Jan 2025 19:59:31 +0000 Subject: [PATCH 250/250] Update copyright dates --- README.md | 2 +- doc/source/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1463c2bde9..4e6953592c 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ The latest generated copy of the documentation can be found at: