From aae3f5bfef27a9bf4c56408df3a6a38f70186e21 Mon Sep 17 00:00:00 2001 From: Eve Date: Tue, 26 Mar 2024 06:43:22 +0000 Subject: [PATCH 001/128] Linux: add first draft of sockscan plugin --- .../framework/plugins/linux/sockscan.py | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 volatility3/framework/plugins/linux/sockscan.py diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py new file mode 100644 index 000000000..52b33d9c1 --- /dev/null +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -0,0 +1,318 @@ +# 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 +import struct +from typing import Callable, Tuple, List, Dict + +from volatility3.framework import interfaces, exceptions, constants, objects +from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import sockstat +from volatility3.framework import symbols +from volatility3.framework import symbols, constants +from volatility3.framework.layers import scanners + +vollog = logging.getLogger(__name__) + + +class Sockscan(plugins.PluginInterface): + """Scans for network connections found in memory layer.""" + + _required_framework_version = (2, 6, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="SockHandlers", component=sockstat.SockHandlers, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self, symbol_table_name: str): + """Scans for sockets. Each row represents a kernel socket. + + Args: + symbol_table_name: The name of the kernel module on which to operate + + Yields: + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[symbol_table_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated + # to the correct size so that they can be accurately located in the physical layer. + if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name): + pack_format = "Q" # 64 bit + else: + pack_format = "I" # 32 bit + + # TODO: Update plugin to support multiple dependencies. e.g. a memory layer and swap file. + # This is a shared problem with psscan and having a generic solution would be useful. + # Find the memory layer to scan, and provide warnings if more than one is located. + if len(kernel_layer.dependencies) > 1: + vollog.warning( + f"Kernel layer depends on multiple layers however only {kernel_layer.dependencies[0]} will be scanned by this plugin." + ) + elif len(kernel_layer.dependencies) == 0: + vollog.error( + f"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + ) + raise exceptions.LayerException( + vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies" + ) + memory_layer_name = kernel_layer.dependencies[0] + memory_layer = self.context.layers[kernel_layer.dependencies[0]] + + # use the init process to build a sock handler + # TODO: look into options so that sockstat.SockHandlers so that process_sock can + # be used without a task object. + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + sock_handler = sockstat.SockHandlers(vmlinux, init_task) + + # set to track seen sockets so that results are not duplicated between methods + sock_physical_addresses = set() + + # get progress_callback in order to use this in the scanners. + # TODO: perhaps add more detail to progress, showing method in progress and number of hits found + progress_callback = self._progress_callback + + # TODO: update scanning logic so that all needles can be scanned for at the same time + # this would allow the results to be shown as the scanning is happening and would + # make the plugin faster. It would require working out which needle caused the match + # and applying the logic at that point to get to the socket. + + # Method 1 - find sockets by file operations, then follow pointers to sockets + file_ops_symbol_names = ["socket_file_ops", "sockfs_dentry_operations"] + file_ops_needles = [] + for symbol_name in file_ops_symbol_names: + + # TODO: handle cases where symbol is not found + needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset + # use canonicalize to set the appropriate sign extension for the addr + addr = kernel_layer.canonicalize(needle_addr) + packed_addr = struct.pack(pack_format, addr) + file_ops_needles.append(packed_addr) + vollog.log( + constants.LOGLEVEL_VVVV, + f"Method 1 will scan for {symbol_name} using the bytes: {packed_addr.hex()}", + ) + + # get file struct to find the offset to the f_op pointer + # this is so that the file object can be created at the correct offset, + # the results of the scanner will be for the f_op member within the file + f_op_offset = vmlinux.get_type("file").members["f_op"][0] + + for addr, _ in memory_layer.scan( + self.context, + scanners.MultiStringScanner(file_ops_needles), + progress_callback, + ): + try: + # create file in the memory_layer, the native layer matches the + # kernel so that pointers can be followed + pfile = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "file", + offset=addr - f_op_offset, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + dentry = pfile.get_dentry() + if not dentry: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(addr)} as unable to locate dentry", + ) + continue + + d_inode = dentry.d_inode + if not d_inode: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(addr)} as unable to locate inode for dentry", + ) + continue + + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + if not (socket and socket.sk): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(addr)} as socket created by LinuxUtilities.container_of is invalid", + ) + continue + + # sucessfully trversed from file to sock, this will exist in the + # kernel layer, and need to be translated to the memory layer. + sock = socket.sk.dereference() + sock_physical_addresses.add(kernel_layer.translate(sock.vol.offset)[0]) + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to follow file at {hex(addr)} to socket due to invalid address: {error}", + ) + + # Method 2 - find sockets by socket destructor directly inside sock objects + socket_destructor_symbol_names = [ + "sock_def_destruct", + "packet_sock_destruct", + "unix_sock_destructor", + "netlink_sock_destruct", + "inet_sock_destruct", + ] + + socket_destructor_needles = [] + for socket_destructor_symbol_name in socket_destructor_symbol_names: + addr = kernel_layer.canonicalize( + vmlinux.get_symbol(socket_destructor_symbol_name).address + + vmlinux.offset + ) + packed_addr = struct.pack(pack_format, addr) + socket_destructor_needles.append(packed_addr) + vollog.log( + constants.LOGLEVEL_VVVV, + f"Method 2 will scan for {socket_destructor_symbol_name} using the bytes: {packed_addr.hex()}", + ) + + # get sock struct to find the offset to the sk_destruct pointer + # this is so that the sock object can be created at the correct offset, + # the results of the scanner will be for the sk_destruct member within the scock + sk_destruct_offset = vmlinux.get_type("sock").members["sk_destruct"][0] + + for addr, _ in memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles), + progress_callback, + ): + sock_physical_addresses.add(addr - sk_destruct_offset) + + # TODO Method 3 - find sock by sk_error_report symbols + # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] + # this would be similar to Method 2, but using a different pointer within sock. + + # now that the set of results has been created, process them and display the results + for addr in sorted(sock_physical_addresses): + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + try: + sock_type = psock.get_type() + + family = psock.get_family() + # remove results with no family + if family == None: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(addr)} as unable to determin family.", + ) + continue + + # TODO: invesitgate options for more invalid address handling in proccess_sock + # and the later formatting on it's results. + sock_fields = sock_handler.process_sock(psock) + # if no sock_fields we're able to be extracted then skip this result. + if not sock_fields: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(addr)} as unable to process with SockHandlers.", + ) + continue + + sock, sock_stat, extended = sock_fields + src, src_port, dst, dst_port, state = sock_stat + protocol = sock.get_protocol() + + # format results + src = NotAvailableValue() if src is None else str(src) + src_port = NotAvailableValue() if src_port is None else str(src_port) + dst = NotAvailableValue() if dst is None else str(dst) + dst_port = NotAvailableValue() if dst_port is None else str(dst_port) + state = NotAvailableValue() if state is None else str(state) + protocol = NotAvailableValue() if protocol is None else str(protocol) + # extended attributes is a dict, so this is formated to string show each + # key and value pair, seperated with a comma. + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else NotAvailableValue() + ) + + # remove empty results + if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, NotAvailableValue) + ): + if state == "UNCONNECTED": + continue + elif src_port == "0" and dst_port == "0": + continue + + fields = ( + format_hints.Hex(sock.vol.offset), + family, + sock_type, + protocol, + src, + src_port, + dst, + dst_port, + state, + socket_filter_str, + ) + + yield (0, fields) + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable create results for socket at {hex(addr)} to invalid address: {error}", + ) + + def run(self): + + tree_grid_args = [ + ("Sock Offset", format_hints.Hex), + ("Family", str), + ("Type", str), + ("Proto", str), + ("Source Addr", str), + ("Source Port", str), + ("Destination Addr", str), + ("Destination Port", str), + ("State", str), + ("Filter", str), + ] + + return TreeGrid( + tree_grid_args, + self._generator(self.config["kernel"]), + ) From 67291ce90fcbea2462293082b1f5d246dfca1b24 Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 27 Mar 2024 09:29:01 +0000 Subject: [PATCH 002/128] Linux: update sockscan to scan memory layer only once for needles --- .../framework/plugins/linux/sockscan.py | 388 ++++++++++-------- 1 file changed, 210 insertions(+), 178 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 52b33d9c1..d02757238 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -4,13 +4,12 @@ import logging import struct -from typing import Callable, Tuple, List, Dict +from typing import List, Set -from volatility3.framework import interfaces, exceptions, constants, objects +from volatility3.framework import exceptions, constants from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility from volatility3.framework.symbols import linux from volatility3.plugins.linux import sockstat from volatility3.framework import symbols @@ -41,6 +40,62 @@ class Sockscan(plugins.PluginInterface): ), ] + def _canonicalize_symbol_addrs( + self, symbol_table_name: List[str], symbol_names: str + ) -> Set[bytes]: + """Takes a list of symbol names and converts the address of each to the bytes + as they would appear in memory so that they can be scanned for. + + Symbols that cannot be found are ignored and not included in the results. + + Args: + symbol_table_name: The name of the kernel module on which to operate + symbol_names: A list of symbol names to be looked up + + Returns: + A set of bytes which are the packed addresses. + """ + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[symbol_table_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated + # to the correct size so that they can be accurately located in the physical layer. + if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name): + pack_format = "Q" # 64 bit + else: + pack_format = "I" # 32 bit + + packed_needles = set() + for symbol_name in symbol_names: + try: + needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset + except exceptions.SymbolError: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to find symbol {symbol_name} this will not be scanned for.", + ) + continue + # use canonicalize to set the appropriate sign extension for the addr + addr = kernel_layer.canonicalize(needle_addr) + packed_addr = struct.pack(pack_format, addr) + packed_needles.add(packed_addr) + vollog.log( + constants.LOGLEVEL_VVVV, + f"Will scan for {symbol_name} using the bytes: {packed_addr.hex()}", + ) + + # make a warning if no symbols at all could be resolved. + if len(packed_needles) == 0: + vollog.warning( + f"_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information." + ) + + return packed_needles + def _generator(self, symbol_table_name: str): """Scans for sockets. Each row represents a kernel socket. @@ -48,6 +103,7 @@ class Sockscan(plugins.PluginInterface): symbol_table_name: The name of the kernel module on which to operate Yields: + addr: Physical offset family: Socket family string (AF_UNIX, AF_INET, etc) sock_type: Socket type string (STREAM, DGRAM, etc) protocol: Protocol string (UDP, TCP, etc) @@ -65,15 +121,9 @@ class Sockscan(plugins.PluginInterface): # kernel layer will be virtual and built ontop of a physical layer. kernel_layer = self.context.layers[vmlinux.layer_name] - # detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated - # to the correct size so that they can be accurately located in the physical layer. - if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name): - pack_format = "Q" # 64 bit - else: - pack_format = "I" # 32 bit - # TODO: Update plugin to support multiple dependencies. e.g. a memory layer and swap file. # This is a shared problem with psscan and having a generic solution would be useful. + # Find the memory layer to scan, and provide warnings if more than one is located. if len(kernel_layer.dependencies) > 1: vollog.warning( @@ -95,91 +145,23 @@ class Sockscan(plugins.PluginInterface): init_task = vmlinux.object_from_symbol(symbol_name="init_task") sock_handler = sockstat.SockHandlers(vmlinux, init_task) - # set to track seen sockets so that results are not duplicated between methods - sock_physical_addresses = set() - # get progress_callback in order to use this in the scanners. # TODO: perhaps add more detail to progress, showing method in progress and number of hits found progress_callback = self._progress_callback - # TODO: update scanning logic so that all needles can be scanned for at the same time - # this would allow the results to be shown as the scanning is happening and would - # make the plugin faster. It would require working out which needle caused the match - # and applying the logic at that point to get to the socket. - # Method 1 - find sockets by file operations, then follow pointers to sockets - file_ops_symbol_names = ["socket_file_ops", "sockfs_dentry_operations"] - file_ops_needles = [] - for symbol_name in file_ops_symbol_names: - - # TODO: handle cases where symbol is not found - needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset - # use canonicalize to set the appropriate sign extension for the addr - addr = kernel_layer.canonicalize(needle_addr) - packed_addr = struct.pack(pack_format, addr) - file_ops_needles.append(packed_addr) - vollog.log( - constants.LOGLEVEL_VVVV, - f"Method 1 will scan for {symbol_name} using the bytes: {packed_addr.hex()}", - ) - + file_ops_symbol_names = [ + "socket_file_ops", + "sockfs_dentry_operations", + ] + file_ops_needles = self._canonicalize_symbol_addrs( + symbol_table_name, file_ops_symbol_names + ) # get file struct to find the offset to the f_op pointer # this is so that the file object can be created at the correct offset, # the results of the scanner will be for the f_op member within the file f_op_offset = vmlinux.get_type("file").members["f_op"][0] - for addr, _ in memory_layer.scan( - self.context, - scanners.MultiStringScanner(file_ops_needles), - progress_callback, - ): - try: - # create file in the memory_layer, the native layer matches the - # kernel so that pointers can be followed - pfile = self.context.object( - vmlinux.symbol_table_name + constants.BANG + "file", - offset=addr - f_op_offset, - layer_name=memory_layer_name, - native_layer_name=vmlinux.layer_name, - ) - dentry = pfile.get_dentry() - if not dentry: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(addr)} as unable to locate dentry", - ) - continue - - d_inode = dentry.d_inode - if not d_inode: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(addr)} as unable to locate inode for dentry", - ) - continue - - socket_alloc = linux.LinuxUtilities.container_of( - d_inode, "socket_alloc", "vfs_inode", vmlinux - ) - socket = socket_alloc.socket - if not (socket and socket.sk): - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(addr)} as socket created by LinuxUtilities.container_of is invalid", - ) - continue - - # sucessfully trversed from file to sock, this will exist in the - # kernel layer, and need to be translated to the memory layer. - sock = socket.sk.dereference() - sock_physical_addresses.add(kernel_layer.translate(sock.vol.offset)[0]) - - except exceptions.InvalidAddressException as error: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Unable to follow file at {hex(addr)} to socket due to invalid address: {error}", - ) - # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_symbol_names = [ "sock_def_destruct", @@ -188,114 +170,164 @@ class Sockscan(plugins.PluginInterface): "netlink_sock_destruct", "inet_sock_destruct", ] - - socket_destructor_needles = [] - for socket_destructor_symbol_name in socket_destructor_symbol_names: - addr = kernel_layer.canonicalize( - vmlinux.get_symbol(socket_destructor_symbol_name).address - + vmlinux.offset - ) - packed_addr = struct.pack(pack_format, addr) - socket_destructor_needles.append(packed_addr) - vollog.log( - constants.LOGLEVEL_VVVV, - f"Method 2 will scan for {socket_destructor_symbol_name} using the bytes: {packed_addr.hex()}", - ) - + socket_destructor_needles = self._canonicalize_symbol_addrs( + symbol_table_name, socket_destructor_symbol_names + ) # get sock struct to find the offset to the sk_destruct pointer # this is so that the sock object can be created at the correct offset, # the results of the scanner will be for the sk_destruct member within the scock sk_destruct_offset = vmlinux.get_type("sock").members["sk_destruct"][0] - for addr, _ in memory_layer.scan( - self.context, - scanners.MultiStringScanner(socket_destructor_needles), - progress_callback, - ): - sock_physical_addresses.add(addr - sk_destruct_offset) - # TODO Method 3 - find sock by sk_error_report symbols # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] # this would be similar to Method 2, but using a different pointer within sock. - # now that the set of results has been created, process them and display the results - for addr in sorted(sock_physical_addresses): - psock = self.context.object( - vmlinux.symbol_table_name + constants.BANG + "sock", - offset=addr, - layer_name=memory_layer_name, - native_layer_name=vmlinux.layer_name, - ) - try: - sock_type = psock.get_type() + # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. + for needle_addr, match in memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), + progress_callback, + ): + psock = None + sock_physical_addr = None - family = psock.get_family() - # remove results with no family - if family == None: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping socket at {hex(addr)} as unable to determin family.", - ) - continue - - # TODO: invesitgate options for more invalid address handling in proccess_sock - # and the later formatting on it's results. - sock_fields = sock_handler.process_sock(psock) - # if no sock_fields we're able to be extracted then skip this result. - if not sock_fields: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping socket at {hex(addr)} as unable to process with SockHandlers.", - ) - continue - - sock, sock_stat, extended = sock_fields - src, src_port, dst, dst_port, state = sock_stat - protocol = sock.get_protocol() - - # format results - src = NotAvailableValue() if src is None else str(src) - src_port = NotAvailableValue() if src_port is None else str(src_port) - dst = NotAvailableValue() if dst is None else str(dst) - dst_port = NotAvailableValue() if dst_port is None else str(dst_port) - state = NotAvailableValue() if state is None else str(state) - protocol = NotAvailableValue() if protocol is None else str(protocol) - # extended attributes is a dict, so this is formated to string show each - # key and value pair, seperated with a comma. - socket_filter_str = ( - ",".join(f"{k}={v}" for k, v in extended.items()) - if extended - else NotAvailableValue() + # if match is from socket_destructor_needles simply calculate the offset + # to the sock + if match in socket_destructor_needles: + sock_physical_addr = needle_addr - sk_destruct_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, ) - # remove empty results - if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( - dst == "0.0.0.0" or isinstance(src, NotAvailableValue) - ): - if state == "UNCONNECTED": - continue - elif src_port == "0" and dst_port == "0": + # if match is from file_ops_needles attempt to walk from file object to + # the sock + if match in file_ops_needles: + try: + # create file in the memory_layer, the native layer matches the + # kernel so that pointers can be followed + sock_physical_addr = needle_addr - f_op_offset + pfile = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "file", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + dentry = pfile.get_dentry() + if not dentry: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate dentry", + ) continue - fields = ( - format_hints.Hex(sock.vol.offset), - family, - sock_type, - protocol, - src, - src_port, - dst, - dst_port, - state, - socket_filter_str, - ) + d_inode = dentry.d_inode + if not d_inode: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry", + ) + continue - yield (0, fields) - except exceptions.InvalidAddressException as error: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Unable create results for socket at {hex(addr)} to invalid address: {error}", - ) + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + if not (socket and socket.sk): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid", + ) + continue + + # sucessfully trversed from file to sock, this will exist in the + # kernel layer, and need to be translated to the memory layer. + psock = socket.sk.dereference() + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", + ) + + if psock is not None: + try: + sock_type = psock.get_type() + + family = psock.get_family() + # remove results with no family + if family == None: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", + ) + continue + + # TODO: invesitgate options for more invalid address handling in proccess_sock + # and the later formatting of it's results. + sock_fields = sock_handler.process_sock(psock) + # if no sock_fields we're able to be extracted then skip this result. + if not sock_fields: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.", + ) + continue + + sock, sock_stat, extended = sock_fields + src, src_port, dst, dst_port, state = sock_stat + protocol = sock.get_protocol() + + # format results + src = NotAvailableValue() if src is None else str(src) + src_port = ( + NotAvailableValue() if src_port is None else str(src_port) + ) + dst = NotAvailableValue() if dst is None else str(dst) + dst_port = ( + NotAvailableValue() if dst_port is None else str(dst_port) + ) + state = NotAvailableValue() if state is None else str(state) + protocol = ( + NotAvailableValue() if protocol is None else str(protocol) + ) + # extended attributes is a dict, so this is formated to string show each + # key and value pair, seperated with a comma. + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else NotAvailableValue() + ) + + # remove empty results + if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, NotAvailableValue) + ): + if state == "UNCONNECTED": + continue + elif src_port == "0" and dst_port == "0": + continue + + fields = ( + format_hints.Hex(sock_physical_addr), + family, + sock_type, + protocol, + src, + src_port, + dst, + dst_port, + state, + socket_filter_str, + ) + + yield (0, fields) + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", + ) def run(self): From 4f1f48087611818f874a033db4257b162e468c3f Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 27 Mar 2024 09:32:49 +0000 Subject: [PATCH 003/128] Linux: add test for sockscan plugin --- test/test_volatility.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index 6e54aa053..0f935546d 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -196,7 +196,7 @@ def test_windows_thrdscan(image, volatility, python): assert out.find(b"\t4\t8") != -1 assert out.find(b"\t4\t12") != -1 assert out.find(b"\t4\t16") != -1 - #assert out.find(b"this raieses AssertionError") != -1 + # assert out.find(b"this raieses AssertionError") != -1 assert rc == 0 @@ -368,6 +368,27 @@ def test_linux_library_list(image, volatility, python): assert rc == 0 +def test_linux_sockscan(image, volatility, python): + # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 + rc, out, err = runvol_plugin("linux.sockscan.Sockscan", image, volatility, python) + + assert re.search( + rb"AF_UNIX\s+STREAM\s+-\s+/tmp/pulse-JldaJj8OxQLa/native\s+14054\s+-\s+14053\s+ESTABLISHED\s+-", + out, + ) + assert re.search( + rb"AF_INET\s+STREAM\s+TCP\s+192.168.201.161\s+22\s+192.168.201.1\s+59982\s+ESTABLISHED\s+-", + out, + ) + assert re.search( + rb"AF_INET\s+STREAM\s+TCP\s+0.0.0.0\s+901\s+0.0.0.0\s+0\s+LISTEN\s+-", + out, + ) + + assert out.count(b"\n") >= 50 + assert rc == 0 + + # MAC From 2c8b757ef02bcf6052705a32da6559789aff4201 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 28 Mar 2024 08:55:07 +0000 Subject: [PATCH 004/128] Linux: update sockscan family check to use 'is None' rather than '== None' as per CodeQL --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index d02757238..89f35e6bb 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -257,7 +257,7 @@ class Sockscan(plugins.PluginInterface): family = psock.get_family() # remove results with no family - if family == None: + if family is None: vollog.log( constants.LOGLEVEL_VVVV, f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", From f3f5650a1d4c5456a8aaa61c58d724bbabf47e97 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 28 Mar 2024 09:03:39 +0000 Subject: [PATCH 005/128] Linux: update sockscan with checks to reduce possible duplication of results --- volatility3/framework/plugins/linux/sockscan.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 89f35e6bb..d3413dea0 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -182,6 +182,9 @@ class Sockscan(plugins.PluginInterface): # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] # this would be similar to Method 2, but using a different pointer within sock. + # add a set of seen addresses to stop possible duplication of results. + seen_sock_physical_addr = set() + # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. for needle_addr, match in memory_layer.scan( self.context, @@ -251,7 +254,8 @@ class Sockscan(plugins.PluginInterface): f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", ) - if psock is not None: + if psock is not None and sock_physical_addr not in seen_sock_physical_addr: + seen_sock_physical_addr.add(sock_physical_addr) try: sock_type = psock.get_type() From fb67d630835e2f7f78b055193de4cfd8696f8f23 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 28 Mar 2024 09:21:54 +0000 Subject: [PATCH 006/128] Linux: update tests for sockscan to be more generic --- test/test_volatility.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index 0f935546d..3d88eeab1 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -372,17 +372,26 @@ def test_linux_sockscan(image, volatility, python): # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 rc, out, err = runvol_plugin("linux.sockscan.Sockscan", image, volatility, python) - assert re.search( - rb"AF_UNIX\s+STREAM\s+-\s+/tmp/pulse-JldaJj8OxQLa/native\s+14054\s+-\s+14053\s+ESTABLISHED\s+-", - out, + # ensure that multiple unix paths for sockets have been found + assert ( + len( + re.findall( + rb"(/[ -~]+?){1,8}", + out, + ) + ) + >= 10 ) - assert re.search( - rb"AF_INET\s+STREAM\s+TCP\s+192.168.201.161\s+22\s+192.168.201.1\s+59982\s+ESTABLISHED\s+-", - out, - ) - assert re.search( - rb"AF_INET\s+STREAM\s+TCP\s+0.0.0.0\s+901\s+0.0.0.0\s+0\s+LISTEN\s+-", - out, + + # ensure that multiple IPv4 addresses have been found + assert ( + len( + re.findall( + rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}", + out, + ) + ) + >= 10 ) assert out.count(b"\n") >= 50 From 3fa63061ddd2bba4a80c843bbb2a2095a7c1e67c Mon Sep 17 00:00:00 2001 From: ikelos Date: Wed, 3 Apr 2024 20:29:24 +0100 Subject: [PATCH 007/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: Donghyun Kim --- volatility3/framework/plugins/linux/sockscan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index d3413dea0..dc657c65b 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -12,7 +12,6 @@ from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.symbols import linux from volatility3.plugins.linux import sockstat -from volatility3.framework import symbols from volatility3.framework import symbols, constants from volatility3.framework.layers import scanners From e4ea19debfeb61d0e441309dcb4edabb22c2dd46 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 2 Aug 2024 11:41:52 +0100 Subject: [PATCH 008/128] Linux: sockscan update based on comments from @gcmoreira, add version, make use of relative_child_offset, fix f string. --- volatility3/framework/plugins/linux/sockscan.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index dc657c65b..cab4b9718 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -22,7 +22,8 @@ class Sockscan(plugins.PluginInterface): """Scans for network connections found in memory layer.""" _required_framework_version = (2, 6, 0) - + _version = (1, 0, 0) + @classmethod def get_requirements(cls): return [ @@ -35,12 +36,12 @@ class Sockscan(plugins.PluginInterface): name="SockHandlers", component=sockstat.SockHandlers, version=(1, 0, 0) ), requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), ] def _canonicalize_symbol_addrs( - self, symbol_table_name: List[str], symbol_names: str + self, symbol_table_name: str, symbol_names: List[str] ) -> Set[bytes]: """Takes a list of symbol names and converts the address of each to the bytes as they would appear in memory so that they can be scanned for. @@ -88,9 +89,9 @@ class Sockscan(plugins.PluginInterface): ) # make a warning if no symbols at all could be resolved. - if len(packed_needles) == 0: + if not packed_needles: vollog.warning( - f"_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information." + "_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information." ) return packed_needles @@ -159,7 +160,7 @@ class Sockscan(plugins.PluginInterface): # get file struct to find the offset to the f_op pointer # this is so that the file object can be created at the correct offset, # the results of the scanner will be for the f_op member within the file - f_op_offset = vmlinux.get_type("file").members["f_op"][0] + f_op_offset = vmlinux.get_type("file").relative_child_offset("f_op") # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_symbol_names = [ @@ -175,7 +176,7 @@ class Sockscan(plugins.PluginInterface): # get sock struct to find the offset to the sk_destruct pointer # this is so that the sock object can be created at the correct offset, # the results of the scanner will be for the sk_destruct member within the scock - sk_destruct_offset = vmlinux.get_type("sock").members["sk_destruct"][0] + sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset("sk_destruct") # TODO Method 3 - find sock by sk_error_report symbols # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] From a3393a971155a0ef095509fdb758f6e2b37994b7 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 2 Aug 2024 11:46:04 +0100 Subject: [PATCH 009/128] Linux: fix black formatting issues --- volatility3/framework/plugins/linux/sockscan.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index cab4b9718..35e8237df 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -23,7 +23,7 @@ class Sockscan(plugins.PluginInterface): _required_framework_version = (2, 6, 0) _version = (1, 0, 0) - + @classmethod def get_requirements(cls): return [ @@ -176,7 +176,9 @@ class Sockscan(plugins.PluginInterface): # get sock struct to find the offset to the sk_destruct pointer # this is so that the sock object can be created at the correct offset, # the results of the scanner will be for the sk_destruct member within the scock - sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset("sk_destruct") + sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset( + "sk_destruct" + ) # TODO Method 3 - find sock by sk_error_report symbols # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] From 3c377320e5b2ddfe6f9e6c9101790af88faa0ade Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 2 Aug 2024 15:41:04 +0100 Subject: [PATCH 010/128] Linux: Begin to breakdown the functions in sockscan plugin --- .../framework/plugins/linux/sockscan.py | 338 ++++++++++-------- 1 file changed, 191 insertions(+), 147 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 35e8237df..57b4daf31 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -96,22 +96,15 @@ class Sockscan(plugins.PluginInterface): return packed_needles - def _generator(self, symbol_table_name: str): - """Scans for sockets. Each row represents a kernel socket. + def _find_memory_layer_name(self, symbol_table_name: str): + """Find the memory layer below the kernel. Only returns a single layer, + and will warn the user if multiple layers are found. Args: - symbol_table_name: The name of the kernel module on which to operate + symbol_table_name: The name of the kernel module on which to operate. - Yields: - addr: Physical offset - family: Socket family string (AF_UNIX, AF_INET, etc) - sock_type: Socket type string (STREAM, DGRAM, etc) - protocol: Protocol string (UDP, TCP, etc) - source addr: Source address string - source port: Source port string (not all of them are int) - destination addr: Destination address string - destination port: Destination port (not all of them are int) - state: State strings (LISTEN, CONNECTED, etc) + Returns: + memory_layer_name: The name of the layer below the kernel to be scanned. """ # get vmlinux module from context in order to build objects and read symbols @@ -136,20 +129,15 @@ class Sockscan(plugins.PluginInterface): raise exceptions.LayerException( vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies" ) + memory_layer_name = kernel_layer.dependencies[0] - memory_layer = self.context.layers[kernel_layer.dependencies[0]] - # use the init process to build a sock handler - # TODO: look into options so that sockstat.SockHandlers so that process_sock can - # be used without a task object. - init_task = vmlinux.object_from_symbol(symbol_name="init_task") - sock_handler = sockstat.SockHandlers(vmlinux, init_task) + return memory_layer_name - # get progress_callback in order to use this in the scanners. - # TODO: perhaps add more detail to progress, showing method in progress and number of hits found - progress_callback = self._progress_callback + def _find_file_ops_needles(self, symbol_table_name: str): + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[symbol_table_name] - # Method 1 - find sockets by file operations, then follow pointers to sockets file_ops_symbol_names = [ "socket_file_ops", "sockfs_dentry_operations", @@ -162,7 +150,12 @@ class Sockscan(plugins.PluginInterface): # the results of the scanner will be for the f_op member within the file f_op_offset = vmlinux.get_type("file").relative_child_offset("f_op") - # Method 2 - find sockets by socket destructor directly inside sock objects + return (file_ops_needles, f_op_offset) + + def _find_sk_destruct_needles(self, symbol_table_name: str): + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[symbol_table_name] + socket_destructor_symbol_names = [ "sock_def_destruct", "packet_sock_destruct", @@ -179,6 +172,173 @@ class Sockscan(plugins.PluginInterface): sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset( "sk_destruct" ) + return (socket_destructor_needles, sk_destruct_offset) + + def _walk_file_ops_needles( + self, symbol_table_name, memory_layer_name, needle_addr, f_op_offset + ): + vmlinux = self.context.modules[symbol_table_name] + try: + # create file in the memory_layer, the native layer matches the + # kernel so that pointers can be followed + sock_physical_addr = needle_addr - f_op_offset + pfile = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "file", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + dentry = pfile.get_dentry() + if not dentry: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate dentry", + ) + return None + + d_inode = dentry.d_inode + if not d_inode: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry", + ) + return None + + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + if not (socket and socket.sk): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid", + ) + return None + + # sucessfully trversed from file to sock, this will exist in the + # kernel layer, and need to be translated to the memory layer. + psock = socket.sk.dereference() + return psock + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", + ) + + def _extract_sock_fields(self, psock, sock_handler): + try: + sock_physical_addr = psock.vol.offset + sock_type = psock.get_type() + + family = psock.get_family() + # remove results with no family + if family is None: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", + ) + return None + + # TODO: invesitgate options for more invalid address handling in proccess_sock + # and the later formatting of it's results. + sock_fields = sock_handler.process_sock(psock) + # if no sock_fields we're able to be extracted then skip this result. + if not sock_fields: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.", + ) + return None + + sock, sock_stat, extended = sock_fields + src, src_port, dst, dst_port, state = sock_stat + protocol = sock.get_protocol() + + # format results + src = NotAvailableValue() if src is None else str(src) + src_port = NotAvailableValue() if src_port is None else str(src_port) + dst = NotAvailableValue() if dst is None else str(dst) + dst_port = NotAvailableValue() if dst_port is None else str(dst_port) + state = NotAvailableValue() if state is None else str(state) + protocol = NotAvailableValue() if protocol is None else str(protocol) + # extended attributes is a dict, so this is formated to string show each + # key and value pair, seperated with a comma. + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else NotAvailableValue() + ) + + # remove empty results + if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, NotAvailableValue) + ): + if state == "UNCONNECTED": + return None + elif src_port == "0" and dst_port == "0": + return None + return ( + format_hints.Hex(sock_physical_addr), + family, + sock_type, + protocol, + src, + src_port, + dst, + dst_port, + state, + socket_filter_str, + ) + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", + ) + + def _generator(self, symbol_table_name: str): + """Scans for sockets. Each row represents a kernel socket. + + Args: + symbol_table_name: The name of the kernel module on which to operate + + Yields: + addr: Physical offset + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[symbol_table_name] + + # get the memory layer that is to be scanned. + memory_layer_name = self._find_memory_layer_name(symbol_table_name) + memory_layer = self.context.layers[memory_layer_name] + + # use the init process to build a sock handler + # TODO: look into options so that sockstat.SockHandlers so that process_sock can + # be used without a task object. + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + sock_handler = sockstat.SockHandlers(vmlinux, init_task) + + # get progress_callback in order to use this in the scanners. + # TODO: perhaps add more detail to progress, showing method in progress and number of hits found + progress_callback = self._progress_callback + + # Method 1 - find sockets by file operations, then follow pointers to sockets + file_ops_needles, f_op_offset = self._find_file_ops_needles(symbol_table_name) + + # Method 2 - find sockets by socket destructor directly inside sock objects + socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( + symbol_table_name + ) # TODO Method 3 - find sock by sk_error_report symbols # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] @@ -196,8 +356,7 @@ class Sockscan(plugins.PluginInterface): psock = None sock_physical_addr = None - # if match is from socket_destructor_needles simply calculate the offset - # to the sock + # if match is from socket_destructor_needles simply calculate the offset to the sock if match in socket_destructor_needles: sock_physical_addr = needle_addr - sk_destruct_offset psock = self.context.object( @@ -207,133 +366,18 @@ class Sockscan(plugins.PluginInterface): native_layer_name=vmlinux.layer_name, ) - # if match is from file_ops_needles attempt to walk from file object to - # the sock + # if match is from file_ops_needles attempt to walk from file object to the sock if match in file_ops_needles: - try: - # create file in the memory_layer, the native layer matches the - # kernel so that pointers can be followed - sock_physical_addr = needle_addr - f_op_offset - pfile = self.context.object( - vmlinux.symbol_table_name + constants.BANG + "file", - offset=sock_physical_addr, - layer_name=memory_layer_name, - native_layer_name=vmlinux.layer_name, - ) - dentry = pfile.get_dentry() - if not dentry: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(needle_addr)} as unable to locate dentry", - ) - continue - - d_inode = dentry.d_inode - if not d_inode: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry", - ) - continue - - socket_alloc = linux.LinuxUtilities.container_of( - d_inode, "socket_alloc", "vfs_inode", vmlinux - ) - socket = socket_alloc.socket - if not (socket and socket.sk): - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid", - ) - continue - - # sucessfully trversed from file to sock, this will exist in the - # kernel layer, and need to be translated to the memory layer. - psock = socket.sk.dereference() - except exceptions.InvalidAddressException as error: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", - ) + psock = self._walk_file_ops_needles( + symbol_table_name, memory_layer_name, needle_addr, f_op_offset + ) if psock is not None and sock_physical_addr not in seen_sock_physical_addr: seen_sock_physical_addr.add(sock_physical_addr) - try: - sock_type = psock.get_type() - - family = psock.get_family() - # remove results with no family - if family is None: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", - ) - continue - - # TODO: invesitgate options for more invalid address handling in proccess_sock - # and the later formatting of it's results. - sock_fields = sock_handler.process_sock(psock) - # if no sock_fields we're able to be extracted then skip this result. - if not sock_fields: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.", - ) - continue - - sock, sock_stat, extended = sock_fields - src, src_port, dst, dst_port, state = sock_stat - protocol = sock.get_protocol() - - # format results - src = NotAvailableValue() if src is None else str(src) - src_port = ( - NotAvailableValue() if src_port is None else str(src_port) - ) - dst = NotAvailableValue() if dst is None else str(dst) - dst_port = ( - NotAvailableValue() if dst_port is None else str(dst_port) - ) - state = NotAvailableValue() if state is None else str(state) - protocol = ( - NotAvailableValue() if protocol is None else str(protocol) - ) - # extended attributes is a dict, so this is formated to string show each - # key and value pair, seperated with a comma. - socket_filter_str = ( - ",".join(f"{k}={v}" for k, v in extended.items()) - if extended - else NotAvailableValue() - ) - - # remove empty results - if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( - dst == "0.0.0.0" or isinstance(src, NotAvailableValue) - ): - if state == "UNCONNECTED": - continue - elif src_port == "0" and dst_port == "0": - continue - - fields = ( - format_hints.Hex(sock_physical_addr), - family, - sock_type, - protocol, - src, - src_port, - dst, - dst_port, - state, - socket_filter_str, - ) + fields = self._extract_sock_fields(psock, sock_handler) + if fields: yield (0, fields) - except exceptions.InvalidAddressException as error: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", - ) def run(self): From dcded3a1fc2688c6cba8bf4e1065f12cbe645db3 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:00:50 +0100 Subject: [PATCH 011/128] Update test/test_volatility.py Co-authored-by: ikelos --- test/test_volatility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index 3d88eeab1..5491a8836 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -196,7 +196,7 @@ def test_windows_thrdscan(image, volatility, python): assert out.find(b"\t4\t8") != -1 assert out.find(b"\t4\t12") != -1 assert out.find(b"\t4\t16") != -1 - # assert out.find(b"this raieses AssertionError") != -1 + # assert out.find(b"this raises AssertionError") != -1 assert rc == 0 From 0fbe03912e20e40ce64090a007038b00554ec2e0 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:01:42 +0100 Subject: [PATCH 012/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 57b4daf31..0ac424481 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -296,6 +296,7 @@ class Sockscan(plugins.PluginInterface): constants.LOGLEVEL_VVVV, f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", ) + return None def _generator(self, symbol_table_name: str): """Scans for sockets. Each row represents a kernel socket. From 95c54351f09de38c0cf4bf0439547f66a33f0b3d Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:01:53 +0100 Subject: [PATCH 013/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 0ac424481..9f756ecc9 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -225,6 +225,7 @@ class Sockscan(plugins.PluginInterface): constants.LOGLEVEL_VVVV, f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", ) + return None def _extract_sock_fields(self, psock, sock_handler): try: From 5e549b2d9204b106c5acdf3248ceba3ef5503cb9 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:10:51 +0100 Subject: [PATCH 014/128] Linux: update sockstat requirements --- .../framework/plugins/linux/sockscan.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 9f756ecc9..11ba8c5fb 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -11,9 +11,10 @@ from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_ from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.symbols import linux -from volatility3.plugins.linux import sockstat -from volatility3.framework import symbols, constants +from volatility3.framework import symbols +from volatility3.plugins.linux import lsof, pslist, sockstat from volatility3.framework.layers import scanners +from volatility3.framework.symbols.linux import network vollog = logging.getLogger(__name__) @@ -33,10 +34,19 @@ class Sockscan(plugins.PluginInterface): architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( - name="SockHandlers", component=sockstat.SockHandlers, version=(1, 0, 0) + name="SockHandlers", component=sockstat.SockHandlers, version=(4, 0, 0) ), requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) + name="lsof", component=lsof.Lsof, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linux_net", component=network.NetSymbols, version=(1, 0, 0) ), ] From 1a57a66ce60533bdc9e33e6ed16f8e536189ff95 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:11:36 +0100 Subject: [PATCH 015/128] Linux: update sockstat ruff issue with f-string using no placeholders --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 11ba8c5fb..27fb5f020 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -134,7 +134,7 @@ class Sockscan(plugins.PluginInterface): ) elif len(kernel_layer.dependencies) == 0: vollog.error( - f"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + "Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." ) raise exceptions.LayerException( vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies" From e55ab3313f9119a7f455a2066f6f953ea92e9e4c Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:16:23 +0100 Subject: [PATCH 016/128] Linux: update tests for sockscan --- test/plugins/linux/linux.py | 34 ++++++++++++++++++++++++++++++++++ test/test_volatility.py | 29 ----------------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..fc01c0c92 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -615,3 +615,37 @@ class TestLinuxPscallstack: rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel", out, ) + + +class TestLinuxSockscan: + def test_linux_sockscan(image, volatility, python): + # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, err = test_volatility.runvol_plugin( + "linux.sockscan.Sockscan", image, volatility, python + ) + + # ensure that multiple unix paths for sockets have been found + assert ( + len( + re.findall( + rb"(/[ -~]+?){1,8}", + out, + ) + ) + >= 10 + ) + + # ensure that multiple IPv4 addresses have been found + assert ( + len( + re.findall( + rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}", + out, + ) + ) + >= 10 + ) + + assert out.count(b"\n") >= 50 + assert rc == 0 diff --git a/test/test_volatility.py b/test/test_volatility.py index 195b51357..9b7aff4be 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -381,32 +381,3 @@ def test_mac_trustedbsd(image, volatility, python): assert out.count(b"\n") > 10 assert rc == 0 - -def test_linux_sockscan(image, volatility, python): - # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 - rc, out, err = runvol_plugin("linux.sockscan.Sockscan", image, volatility, python) - - # ensure that multiple unix paths for sockets have been found - assert ( - len( - re.findall( - rb"(/[ -~]+?){1,8}", - out, - ) - ) - >= 10 - ) - - # ensure that multiple IPv4 addresses have been found - assert ( - len( - re.findall( - rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}", - out, - ) - ) - >= 10 - ) - - assert out.count(b"\n") >= 50 - assert rc == 0 From ca584cce7403dec5367e5fb2b51064277fb12864 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:23:21 +0100 Subject: [PATCH 017/128] Linux: Update linux sockscan to work with SockHandlers v4.0.0 --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 27fb5f020..915a8e834 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -338,7 +338,7 @@ class Sockscan(plugins.PluginInterface): # TODO: look into options so that sockstat.SockHandlers so that process_sock can # be used without a task object. init_task = vmlinux.object_from_symbol(symbol_name="init_task") - sock_handler = sockstat.SockHandlers(vmlinux, init_task) + sock_handler = sockstat.SockHandlers(self.context, symbol_table_name, init_task) # get progress_callback in order to use this in the scanners. # TODO: perhaps add more detail to progress, showing method in progress and number of hits found From 704f0f023d7507b18a64608935c729f474f57d82 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:35:04 +0100 Subject: [PATCH 018/128] Linux: Fix incorrect first parameter name in test_linux_sockscan --- test/plugins/linux/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index fc01c0c92..868b439fb 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -618,7 +618,7 @@ class TestLinuxPscallstack: class TestLinuxSockscan: - def test_linux_sockscan(image, volatility, python): + def test_linux_sockscan(self, volatility, python): # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 image = LinuxSamples.LINUX_GENERIC.value.path rc, out, err = test_volatility.runvol_plugin( From d532f3745438907aa654a5436bfcd04846763bf6 Mon Sep 17 00:00:00 2001 From: eve Date: Thu, 24 Apr 2025 09:37:39 +0100 Subject: [PATCH 019/128] Linux: sockstat contiuned work to breakdown large generator function --- .../framework/plugins/linux/sockscan.py | 128 +++++++++--------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 915a8e834..530b36c04 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -309,84 +309,86 @@ class Sockscan(plugins.PluginInterface): ) return None - def _generator(self, symbol_table_name: str): - """Scans for sockets. Each row represents a kernel socket. - - Args: - symbol_table_name: The name of the kernel module on which to operate - - Yields: - addr: Physical offset - family: Socket family string (AF_UNIX, AF_INET, etc) - sock_type: Socket type string (STREAM, DGRAM, etc) - protocol: Protocol string (UDP, TCP, etc) - source addr: Source address string - source port: Source port string (not all of them are int) - destination addr: Destination address string - destination port: Destination port (not all of them are int) - state: State strings (LISTEN, CONNECTED, etc) - """ - - # get vmlinux module from context in order to build objects and read symbols + def _build_sock_handler(self, symbol_table_name: str): vmlinux = self.context.modules[symbol_table_name] + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + return sockstat.SockHandlers(self.context, symbol_table_name, init_task) - # get the memory layer that is to be scanned. + def _scan_for_sockets( + self, memory_layer, file_ops_needles, socket_destructor_needles + ): + return memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), + self._progress_callback, + ) + + def _parse_scanner_result( + self, + match, + needle_addr, + file_ops_needles, + socket_destructor_needles, + symbol_table_name, + memory_layer_name, + f_op_offset, + sk_destruct_offset, + ): + + if match in socket_destructor_needles: + sock_physical_addr = needle_addr - sk_destruct_offset + psock = self._get_socket_from_sk_destruct( + sock_physical_addr, symbol_table_name, memory_layer_name + ) + elif match in file_ops_needles: + psock = self._walk_file_ops_needles( + symbol_table_name, memory_layer_name, needle_addr, f_op_offset + ) + sock_physical_addr = psock.vol.offset if psock else None + else: + psock = sock_physical_addr = None + + return psock, sock_physical_addr + + def _get_socket_from_sk_destruct( + self, sock_physical_addr, symbol_table_name, memory_layer_name + ): + vmlinux = self.context.modules[symbol_table_name] + return self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + def _generator(self, symbol_table_name: str): memory_layer_name = self._find_memory_layer_name(symbol_table_name) memory_layer = self.context.layers[memory_layer_name] + sock_handler = self._build_sock_handler(symbol_table_name) - # use the init process to build a sock handler - # TODO: look into options so that sockstat.SockHandlers so that process_sock can - # be used without a task object. - init_task = vmlinux.object_from_symbol(symbol_name="init_task") - sock_handler = sockstat.SockHandlers(self.context, symbol_table_name, init_task) - - # get progress_callback in order to use this in the scanners. - # TODO: perhaps add more detail to progress, showing method in progress and number of hits found - progress_callback = self._progress_callback - - # Method 1 - find sockets by file operations, then follow pointers to sockets file_ops_needles, f_op_offset = self._find_file_ops_needles(symbol_table_name) - - # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( symbol_table_name ) - # TODO Method 3 - find sock by sk_error_report symbols - # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] - # this would be similar to Method 2, but using a different pointer within sock. - - # add a set of seen addresses to stop possible duplication of results. seen_sock_physical_addr = set() - # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. - for needle_addr, match in memory_layer.scan( - self.context, - scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), - progress_callback, + for needle_addr, match in self._scan_for_sockets( + memory_layer, file_ops_needles, socket_destructor_needles ): - psock = None - sock_physical_addr = None + psock, sock_physical_addr = self._parse_scanner_result( + match, + needle_addr, + file_ops_needles, + socket_destructor_needles, + symbol_table_name, + memory_layer_name, + f_op_offset, + sk_destruct_offset, + ) - # if match is from socket_destructor_needles simply calculate the offset to the sock - if match in socket_destructor_needles: - sock_physical_addr = needle_addr - sk_destruct_offset - psock = self.context.object( - vmlinux.symbol_table_name + constants.BANG + "sock", - offset=sock_physical_addr, - layer_name=memory_layer_name, - native_layer_name=vmlinux.layer_name, - ) - - # if match is from file_ops_needles attempt to walk from file object to the sock - if match in file_ops_needles: - psock = self._walk_file_ops_needles( - symbol_table_name, memory_layer_name, needle_addr, f_op_offset - ) - - if psock is not None and sock_physical_addr not in seen_sock_physical_addr: + if psock and sock_physical_addr not in seen_sock_physical_addr: seen_sock_physical_addr.add(sock_physical_addr) - fields = self._extract_sock_fields(psock, sock_handler) if fields: yield (0, fields) From 787f3b629ae14427576406d5d36b9d234f839eae Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 11:10:05 +0100 Subject: [PATCH 020/128] Revert "Linux: sockstat contiuned work to breakdown large generator function" This reverts commit d532f3745438907aa654a5436bfcd04846763bf6. --- .../framework/plugins/linux/sockscan.py | 130 +++++++++--------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 530b36c04..915a8e834 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -309,86 +309,84 @@ class Sockscan(plugins.PluginInterface): ) return None - def _build_sock_handler(self, symbol_table_name: str): - vmlinux = self.context.modules[symbol_table_name] - init_task = vmlinux.object_from_symbol(symbol_name="init_task") - return sockstat.SockHandlers(self.context, symbol_table_name, init_task) - - def _scan_for_sockets( - self, memory_layer, file_ops_needles, socket_destructor_needles - ): - return memory_layer.scan( - self.context, - scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), - self._progress_callback, - ) - - def _parse_scanner_result( - self, - match, - needle_addr, - file_ops_needles, - socket_destructor_needles, - symbol_table_name, - memory_layer_name, - f_op_offset, - sk_destruct_offset, - ): - - if match in socket_destructor_needles: - sock_physical_addr = needle_addr - sk_destruct_offset - psock = self._get_socket_from_sk_destruct( - sock_physical_addr, symbol_table_name, memory_layer_name - ) - elif match in file_ops_needles: - psock = self._walk_file_ops_needles( - symbol_table_name, memory_layer_name, needle_addr, f_op_offset - ) - sock_physical_addr = psock.vol.offset if psock else None - else: - psock = sock_physical_addr = None - - return psock, sock_physical_addr - - def _get_socket_from_sk_destruct( - self, sock_physical_addr, symbol_table_name, memory_layer_name - ): - vmlinux = self.context.modules[symbol_table_name] - return self.context.object( - vmlinux.symbol_table_name + constants.BANG + "sock", - offset=sock_physical_addr, - layer_name=memory_layer_name, - native_layer_name=vmlinux.layer_name, - ) - def _generator(self, symbol_table_name: str): + """Scans for sockets. Each row represents a kernel socket. + + Args: + symbol_table_name: The name of the kernel module on which to operate + + Yields: + addr: Physical offset + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[symbol_table_name] + + # get the memory layer that is to be scanned. memory_layer_name = self._find_memory_layer_name(symbol_table_name) memory_layer = self.context.layers[memory_layer_name] - sock_handler = self._build_sock_handler(symbol_table_name) + # use the init process to build a sock handler + # TODO: look into options so that sockstat.SockHandlers so that process_sock can + # be used without a task object. + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + sock_handler = sockstat.SockHandlers(self.context, symbol_table_name, init_task) + + # get progress_callback in order to use this in the scanners. + # TODO: perhaps add more detail to progress, showing method in progress and number of hits found + progress_callback = self._progress_callback + + # Method 1 - find sockets by file operations, then follow pointers to sockets file_ops_needles, f_op_offset = self._find_file_ops_needles(symbol_table_name) + + # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( symbol_table_name ) + # TODO Method 3 - find sock by sk_error_report symbols + # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] + # this would be similar to Method 2, but using a different pointer within sock. + + # add a set of seen addresses to stop possible duplication of results. seen_sock_physical_addr = set() - for needle_addr, match in self._scan_for_sockets( - memory_layer, file_ops_needles, socket_destructor_needles + # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. + for needle_addr, match in memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), + progress_callback, ): - psock, sock_physical_addr = self._parse_scanner_result( - match, - needle_addr, - file_ops_needles, - socket_destructor_needles, - symbol_table_name, - memory_layer_name, - f_op_offset, - sk_destruct_offset, - ) + psock = None + sock_physical_addr = None - if psock and sock_physical_addr not in seen_sock_physical_addr: + # if match is from socket_destructor_needles simply calculate the offset to the sock + if match in socket_destructor_needles: + sock_physical_addr = needle_addr - sk_destruct_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + # if match is from file_ops_needles attempt to walk from file object to the sock + if match in file_ops_needles: + psock = self._walk_file_ops_needles( + symbol_table_name, memory_layer_name, needle_addr, f_op_offset + ) + + if psock is not None and sock_physical_addr not in seen_sock_physical_addr: seen_sock_physical_addr.add(sock_physical_addr) + fields = self._extract_sock_fields(psock, sock_handler) if fields: yield (0, fields) From ab5dac10d936c5b668a9c69e6acbada06abcd662 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 11:54:55 +0100 Subject: [PATCH 021/128] Linux: fix virtual to physical offsets for sockscan file_ops method --- .../framework/plugins/linux/sockscan.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 915a8e834..a123e74cd 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -227,7 +227,25 @@ class Sockscan(plugins.PluginInterface): # sucessfully trversed from file to sock, this will exist in the # kernel layer, and need to be translated to the memory layer. - psock = socket.sk.dereference() + vsock = socket.sk.dereference() + + # get virtual offset + virtual_sock_offset = vsock.vol.offset + + # translate this offset to physical + native_layer = self.context.layers[vmlinux.layer_name] + physical_sock_offset, _physical_layer_name = native_layer.translate( + virtual_sock_offset + ) + + # build sock on the memory_layer using the physical_sock_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=physical_sock_offset, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + return psock except exceptions.InvalidAddressException as error: @@ -336,7 +354,7 @@ class Sockscan(plugins.PluginInterface): # use the init process to build a sock handler # TODO: look into options so that sockstat.SockHandlers so that process_sock can - # be used without a task object. + # be used without a task object. init_task = vmlinux.object_from_symbol(symbol_name="init_task") sock_handler = sockstat.SockHandlers(self.context, symbol_table_name, init_task) From 0d32cac06786eb0b6e449ca65057072278213296 Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 11:58:29 +0100 Subject: [PATCH 022/128] Linux: fix sockscan use of direct imports of TreeGrid and NotAvailableValue --- .../framework/plugins/linux/sockscan.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index a123e74cd..b9a0d854d 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -7,7 +7,8 @@ import struct from typing import List, Set from volatility3.framework import exceptions, constants -from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints +from volatility3.framework import renderers +from volatility3.framework.renderers import format_hints from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.symbols import linux @@ -285,23 +286,29 @@ class Sockscan(plugins.PluginInterface): protocol = sock.get_protocol() # format results - src = NotAvailableValue() if src is None else str(src) - src_port = NotAvailableValue() if src_port is None else str(src_port) - dst = NotAvailableValue() if dst is None else str(dst) - dst_port = NotAvailableValue() if dst_port is None else str(dst_port) - state = NotAvailableValue() if state is None else str(state) - protocol = NotAvailableValue() if protocol is None else str(protocol) + src = renderers.NotAvailableValue() if src is None else str(src) + src_port = ( + renderers.NotAvailableValue() if src_port is None else str(src_port) + ) + dst = renderers.NotAvailableValue() if dst is None else str(dst) + dst_port = ( + renderers.NotAvailableValue() if dst_port is None else str(dst_port) + ) + state = renderers.NotAvailableValue() if state is None else str(state) + protocol = ( + renderers.NotAvailableValue() if protocol is None else str(protocol) + ) # extended attributes is a dict, so this is formated to string show each # key and value pair, seperated with a comma. socket_filter_str = ( ",".join(f"{k}={v}" for k, v in extended.items()) if extended - else NotAvailableValue() + else renderers.NotAvailableValue() ) # remove empty results - if (src == "0.0.0.0" or isinstance(src, NotAvailableValue)) and ( - dst == "0.0.0.0" or isinstance(src, NotAvailableValue) + if (src == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue) ): if state == "UNCONNECTED": return None @@ -424,7 +431,7 @@ class Sockscan(plugins.PluginInterface): ("Filter", str), ] - return TreeGrid( + return renderers.TreeGrid( tree_grid_args, self._generator(self.config["kernel"]), ) From 6f11161bd5388f3bf5ed3b97ff8841e11b65810f Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 11:59:43 +0100 Subject: [PATCH 023/128] Linux: add MultiStringScanner requirement for sockscan --- volatility3/framework/plugins/linux/sockscan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index b9a0d854d..943e7447b 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -49,6 +49,11 @@ class Sockscan(plugins.PluginInterface): requirements.VersionRequirement( name="linux_net", component=network.NetSymbols, version=(1, 0, 0) ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), ] def _canonicalize_symbol_addrs( From 7cfc03da5dff89d608a97e08e4313ce88597a20c Mon Sep 17 00:00:00 2001 From: eve Date: Fri, 25 Apr 2025 12:13:16 +0100 Subject: [PATCH 024/128] Linux: update sockscan to use minor version for the requirements this has been tested with --- volatility3/framework/plugins/linux/sockscan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 943e7447b..4f16920cf 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -41,10 +41,10 @@ class Sockscan(plugins.PluginInterface): name="lsof", component=lsof.Lsof, version=(2, 0, 0) ), requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(4, 0, 0) + name="pslist", component=pslist.PsList, version=(4, 1, 0) ), requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) ), requirements.VersionRequirement( name="linux_net", component=network.NetSymbols, version=(1, 0, 0) From 0d272c919a0a30117264c8b336c9cb1ec65406e8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 21 May 2025 18:48:51 +0200 Subject: [PATCH 025/128] enhance VMA smearing protection --- .../symbols/linux/extensions/__init__.py | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index c980ee6b1..a97cf2a5a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1094,14 +1094,11 @@ class mm_struct(objects.StructType): vm_area_struct objects """ for vma in self._do_get_vma_iter(): - try: - vma.vm_start - vma.vm_end - vma.get_protection() - - yield vma - except exceptions.InvalidAddressException: + if not vma.is_valid(): vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}") + continue + + yield vma class super_block(objects.StructType): @@ -1237,6 +1234,39 @@ class vm_area_struct(objects.StructType): retval = retval + "-" return retval + def is_valid(self) -> bool: + """Validate a VMA struct to prevent processing smeared entries.""" + try: + start = self.vm_start + end = self.vm_end + self.get_protection() + except exceptions.InvalidAddressException: + return False + + layer = self._context.layers[self.vol.layer_name] + length = end - start + if ( + (start > end) + or (start == 0 and length == 0) + or (length % layer.page_size != 0) + ): + return False + + if self.vm_file != 0: + try: + inode = self.vm_file.get_inode() + except exceptions.InvalidAddressException: + return False + + # Verify that a file-backed VMA's page offset + # is not greater than the size of the file's inode. + # Check only inode sizes greater than 0 to account for + # special devices (e.g. "/dev/dri/card0") and prevent false negatives. + if inode.i_size > 0 and self.get_page_offset() > inode.i_size: + return False + + return True + # only parse the rwx bits def get_protection(self) -> str: return self._parse_flags(self.vm_flags & 0b1111, vm_area_struct.perm_flags) From 75293ec93185da3cb09f072d6a07a4a67dc08136 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 21 May 2025 18:49:31 +0200 Subject: [PATCH 026/128] bump: 2.26.2 -> 2.27.0 --- volatility3/framework/constants/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 07b9e45ec..7f71c277e 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 = 26 # Number of changes that only add to the interface -VERSION_PATCH = 2 # Number of changes that do not change the interface +VERSION_MINOR = 27 # 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 = ( From b2e836464694b8cf0e069e26430b767873e4cba6 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 10 Jun 2025 15:59:22 +0100 Subject: [PATCH 027/128] Windows: Fix pe_symbols type checking --- volatility3/framework/plugins/windows/pe_symbols.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 00c6fd868..b21e39a8c 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -709,17 +709,15 @@ class PESymbols(interfaces.plugins.PluginInterface): # symbol_info will be a symbol name or address requested for symbol_info in wanted_symbols: - if ( - wanted_type == wanted_names_identifier - and type(symbol_info) not in valid_name_types + if wanted_type == wanted_names_identifier and not isinstance( + symbol_info, tuple(valid_name_types) ): raise ValueError( f"The requested symbol name has a type of {type(symbol_info)} which is not in the allowed set of {valid_name_types}" ) - elif ( - wanted_type == wanted_addresses_identifier - and type(symbol_info) not in valid_address_types + elif wanted_type == wanted_addresses_identifier and not isinstance( + symbol_info, tuple(valid_address_types) ): raise ValueError( f"The requested address has a type of {type(symbol_info)} which is not in the allowed set of {valid_address_types}" From 735f5933ba72a261d2687ca2ba0fdeb0cd72a15e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:05:28 +0200 Subject: [PATCH 028/128] make module_sect_attr and bin_attribute optional --- volatility3/framework/symbols/linux/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 19fb8f1d4..7560da3ec 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -52,7 +52,6 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("idr", extensions.IDR) self.set_type_class("address_space", extensions.address_space) self.set_type_class("page", extensions.page) - self.set_type_class("module_sect_attr", extensions.module_sect_attr) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) @@ -61,6 +60,8 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) self.optional_set_type_class("scatterlist", extensions.scatterlist) + self.optional_set_type_class("module_sect_attr", extensions.module_sect_attr) + self.optional_set_type_class("bin_attribute", extensions.bin_attribute) # kernels >= 4.18 self.optional_set_type_class("timespec64", extensions.timespec64) From ac4326f4780dce79d31bb6c92f1c00a3961c1f7f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:06:00 +0200 Subject: [PATCH 029/128] determine sect_attrs.attrs subtype dynamically --- .../symbols/linux/extensions/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0136d749a..f7c60d617 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -211,9 +211,7 @@ class module(generic.GenericIntelProcess): symbol_table_name + constants.BANG + "array", layer_name=self.vol.layer_name, offset=self.sect_attrs.attrs.vol.offset, - subtype=self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "module_sect_attr" - ), + subtype=self.sect_attrs.attrs.vol.subtype, count=self.number_of_sections, ) @@ -3189,3 +3187,17 @@ class module_sect_attr(objects.StructType): ) return None + +class bin_attribute(objects.StructType): + def get_name(self) -> Optional[str]: + """ + Performs extraction of the bin_attribute name + """ + if hasattr(self, "attr"): + try: + return utility.pointer_to_string(self.attr.name, count=32) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + return None + + return None \ No newline at end of file From 4f296f7f2d8b2d146b59c37ee954489bd9d7a321 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 15:08:27 +0200 Subject: [PATCH 030/128] black formatting --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index f7c60d617..0fed4d71f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3188,6 +3188,7 @@ class module_sect_attr(objects.StructType): return None + class bin_attribute(objects.StructType): def get_name(self) -> Optional[str]: """ @@ -3197,7 +3198,9 @@ class bin_attribute(objects.StructType): try: return utility.pointer_to_string(self.attr.name, count=32) except exceptions.InvalidAddressException: - vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + vollog.debug( + f"Invalid attr name for bin_attribute at {self.vol.offset:#x}" + ) return None - return None \ No newline at end of file + return None From 75e0d041a2ac1df274ffa845961477934d1b5652 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 17:11:44 +0200 Subject: [PATCH 031/128] sections manual enumeration adjustment --- .../symbols/linux/extensions/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0fed4d71f..2830a8e58 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -179,16 +179,27 @@ class module(generic.GenericIntelProcess): return None def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: - """Try to determine the number of valid sections""" + """Try to determine the number of valid sections. Support for kernels > 6.14-rc1. + + Resources: + - https://github.com/torvalds/linux/commit/d8959b947a8dfab1047c6fd5e982808f65717bfe + - https://github.com/torvalds/linux/commit/e0349c46cb4fbbb507fa34476bd70f9c82bad359 + """ + + if grp.has_member("bin_attrs"): + arr_offset = grp.bin_attrs + else: + arr_offset = grp.attrs + symbol_table_name = self.get_symbol_table_name() arr = self._context.object( symbol_table_name + constants.BANG + "array", layer_name=self.vol.layer_name, - offset=grp.attrs, + offset=arr_offset, subtype=self._context.symbol_space.get_type( symbol_table_name + constants.BANG + "pointer" ), - count=25, + count=50, ) idx = 0 @@ -198,6 +209,7 @@ class module(generic.GenericIntelProcess): @functools.cached_property def number_of_sections(self) -> int: + # Dropped in 6.14-rc1: d8959b947a8dfab1047c6fd5e982808f65717bfe if self.sect_attrs.has_member("nsections"): return self.sect_attrs.nsections From 825cb321a58ae211a60a1419fa1ea98d7122ceb5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 15 Apr 2025 17:11:53 +0200 Subject: [PATCH 032/128] sections manual enumeration adjustment --- .../framework/symbols/linux/utilities/module_extract.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e55f77668..c2732f216 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,7 +38,7 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) @@ -60,9 +60,14 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): count = 0 try: + if grp.has_member("bin_attrs"): + arr_offset = grp.bin_attrs + else: + arr_offset = grp.attrs + array = kernel.object( object_type="array", - offset=grp.attrs, + offset=arr_offset, sub_type=kernel.get_type("pointer"), count=50, absolute=True, From 9478d36d7cc13123cfe20e5a369cfc58a719bade Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:31:25 +0200 Subject: [PATCH 033/128] get_sections() sanity check --- volatility3/framework/symbols/linux/extensions/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 2830a8e58..d41386e7e 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -217,6 +217,11 @@ class module(generic.GenericIntelProcess): def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]: """Get a list of section attributes for the given module.""" + if self.number_of_sections == 0: + vollog.debug( + f"Invalid number of sections ({self.number_of_sections}) for module at offset {self.vol.offset:#x}" + ) + return [] symbol_table_name = self.get_symbol_table_name() arr = self._context.object( From 70dfa09b26c66bcb8996e6399522e1053dd121b1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:31:34 +0200 Subject: [PATCH 034/128] add bin_attribute address virtual member --- volatility3/framework/symbols/linux/extensions/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d41386e7e..b02dc3dd2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3221,3 +3221,10 @@ class bin_attribute(objects.StructType): return None return None + + @property + def address(self) -> int: + """Equivalent to module_sect_attr.address: + - https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f + """ + return self.private From d8518b387896a18d4ea04a75791f12fe8d28f062 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:32:22 +0200 Subject: [PATCH 035/128] 1774: consolidate module helpers --- .../symbols/linux/utilities/module_extract.py | 105 ++---------------- 1 file changed, 7 insertions(+), 98 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index c2732f216..64740b682 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,56 +38,11 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 1) + _version = (1, 0, 2) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) - @classmethod - def _get_module_section_count( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - grp: interfaces.objects.ObjectInterface, - ) -> int: - """ - Used to manually determine the section count for kernels that do not track - this count directly within the attribute structures - """ - kernel = context.modules[vmlinux_name] - - count = 0 - - try: - if grp.has_member("bin_attrs"): - arr_offset = grp.bin_attrs - else: - arr_offset = grp.attrs - - array = kernel.object( - object_type="array", - offset=arr_offset, - sub_type=kernel.get_type("pointer"), - count=50, - absolute=True, - ) - - # Walk up to 50 sections counting until we reach the end or a page fault - for sect in array: - if sect.vol.offset == 0: - break - - count += 1 - - except exceptions.InvalidAddressException: - # Use whatever count we reached before the error - vollog.debug( - f"Exception hit counting sections for module at {module.vol.offset:#x}" - ) - - return count - @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -266,54 +221,6 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): return sym_table_data - @classmethod - def _enumerate_original_sections( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - ) -> Optional[Dict[int, str]]: - """ - Enumerates the module's sections as maintained by the kernel after load time - 'Early' sections like .init.text and .init.data are discarded after module - initialization, so they are not expected to be in memory during extraction - """ - if hasattr(module.sect_attrs, "nsections"): - num_sections = module.sect_attrs.nsections - else: - num_sections = cls._get_module_section_count( - context, vmlinux_name, module.sect_attrs.grp - ) - - if num_sections > 1024 or num_sections == 0: - vollog.debug( - f"Invalid number of sections ({num_sections}) for module at offset {module.vol.offset:#x}" - ) - return None - - vmlinux = context.modules[vmlinux_name] - - # This is declared as a zero sized array, so we create ourselves - attribute_type = module.sect_attrs.attrs.vol.subtype - - sect_array = vmlinux.object( - object_type="array", - subtype=attribute_type, - offset=module.sect_attrs.attrs.vol.offset, - count=num_sections, - absolute=True, - ) - - sections: Dict[int, str] = {} - - # for each section, gather its name and address - for index, section in enumerate(sect_array): - name = section.get_name() - - sections[section.address] = name - - return sections - @classmethod def _parse_sections( cls, @@ -330,10 +237,12 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ - original_sections = cls._enumerate_original_sections( - context, vmlinux_name, module - ) - if original_sections is None: + original_sections = {} + for index, section in enumerate(module.get_sections()): + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: return None kernel = context.modules[vmlinux_name] From c223a12be9ceffd676154713001dda8ce5c5a078 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:33:00 +0200 Subject: [PATCH 036/128] handle None in _parse_sections caller --- .../framework/symbols/linux/utilities/module_extract.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 64740b682..500e81948 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -616,9 +616,10 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): return None # Gather sections - updated_sections, strtab_index, symtab_index = cls._parse_sections( - context, vmlinux_name, module - ) + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result kernel = context.modules[vmlinux_name] From 522c2d435c7a38a5d37b4afd8180a46f96eef779 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 2 May 2025 14:36:20 +0200 Subject: [PATCH 037/128] rollback to already patched version --- volatility3/framework/symbols/linux/utilities/module_extract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 500e81948..e4f5705cf 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -38,7 +38,7 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 2) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) From 910488235478410cd9d8920546f9b91dfc4b2c99 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 5 May 2025 12:02:41 +0200 Subject: [PATCH 038/128] add ATTRIBUTE_NAME_MAX_SIZE constant --- volatility3/framework/constants/linux/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 3f8c52b43..72440e23e 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -432,6 +432,17 @@ VMCOREINFO_MAGIC = b"VMCOREINFO\x00" VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00" OSRELEASE_TAG = b"OSRELEASE=" +ATTRIBUTE_NAME_MAX_SIZE = 255 +""" +In 5.9-rc1+, the Linux kernel limits the READ size of a section bin_attribute name to MODULE_SECT_READ_SIZE: + +- https://elixir.bootlin.com/linux/v6.15-rc4/source/kernel/module/sysfs.c#L106 +- https://github.com/torvalds/linux/commit/11990a5bd7e558e9203c1070fc52fb6f0488e75b + +However, the raw section name loaded from the .ko ELF can in theory be thousands of characters, +and unless we do a NULL terminated search we can't set a perfect value. +""" + @dataclass class TaintFlag: From a5d1641db338c1aeae95ffe618908cf5d06e1fe8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 5 May 2025 12:03:03 +0200 Subject: [PATCH 039/128] use ATTRIBUTE_NAME_MAX_SIZE constant --- .../symbols/linux/extensions/__init__.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b02dc3dd2..037ca1f6b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3172,7 +3172,9 @@ class module_sect_attr(objects.StructType): """ if hasattr(self, "battr"): try: - return utility.pointer_to_string(self.battr.attr.name, count=32) + return utility.pointer_to_string( + self.battr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # if battr is present then its name attribute needs to be valid vollog.debug(f"Invalid battr name for section at {self.vol.offset:#x}") @@ -3180,14 +3182,18 @@ class module_sect_attr(objects.StructType): elif self.name.vol.type_name == "array": try: - return utility.array_to_string(self.name, count=32) + return utility.array_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # specifically do not return here to give `mattr` a chance vollog.debug(f"Invalid direct name for section at {self.vol.offset:#x}") elif self.name.vol.type_name == "pointer": try: - return utility.pointer_to_string(self.name, count=32) + return utility.pointer_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # specifically do not return here to give `mattr` a chance vollog.debug( @@ -3197,7 +3203,9 @@ class module_sect_attr(objects.StructType): # if everything else failed... if hasattr(self, "mattr"): try: - return utility.pointer_to_string(self.mattr.attr.name, count=32) + return utility.pointer_to_string( + self.mattr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: vollog.debug( f"Unresolvable name for for section at {self.vol.offset:#x}" @@ -3213,7 +3221,9 @@ class bin_attribute(objects.StructType): """ if hasattr(self, "attr"): try: - return utility.pointer_to_string(self.attr.name, count=32) + return utility.pointer_to_string( + self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: vollog.debug( f"Invalid attr name for bin_attribute at {self.vol.offset:#x}" From 2cda8e3cb39551e7b2c94297a75d88499423ca0e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Tue, 6 May 2025 01:03:35 +0200 Subject: [PATCH 040/128] make binary attributes iteration NULL terminated --- .../symbols/linux/extensions/__init__.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 037ca1f6b..22eb46460 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -191,20 +191,27 @@ class module(generic.GenericIntelProcess): else: arr_offset = grp.attrs - symbol_table_name = self.get_symbol_table_name() - arr = self._context.object( - symbol_table_name + constants.BANG + "array", - layer_name=self.vol.layer_name, - offset=arr_offset, - subtype=self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ), - count=50, - ) + if not arr_offset.is_readable(): + vollog.log( + constants.LOGLEVEL_V, + f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}", + ) + return 0 + entry = arr_offset.dereference() + symbol_table_name = self.get_symbol_table_name() idx = 0 - while arr[idx] and arr[idx].is_readable(): - idx = idx + 1 + while entry.is_readable(): + idx += 1 + entry = self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.layer_name, + offset=entry.vol.offset + + self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ).size, + ) + return idx @functools.cached_property From 1c11791b7fbb2b1dd8c2589f986f097b1a607da8 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:10:16 +0200 Subject: [PATCH 041/128] add dynamically_sized_array_of_pointers() helper --- volatility3/framework/objects/utility.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 59ac0ee55..9e9fef33a 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -250,3 +250,52 @@ def array_of_pointers( ).clone() subtype_pointer.update_vol(subtype=subtype) return array.cast("array", count=count, subtype=subtype_pointer) + + +def dynamically_sized_array_of_pointers( + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table_name: str, + array_offset: int, + stop_value: int = 0, + iterator_guard_value: int = None, + stop_on_invalid_pointers: bool = True, +) -> interfaces.objects.ObjectInterface: + """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). + + Args: + context: The context on which to operate. + layer_name: The layer on which the array should be constructed. + symbol_table_name: The symbol table to use to construct object types. + array_offset: The array offset within the layer, from which to start iterating. + stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. + stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays + that are known to have smeared entries before the end. + """ + pointer_type = context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ) + entry = context.object( + pointer_type, + layer_name=layer_name, + offset=array_offset, + ) + i = 0 + array = [] + # entry and entry.vol.offset aren't the same thing, as + # - entry is naturally represented by the address that the pointer refers to; + # - entry.vol.offset is the offset at which the pointer lives. + while entry != stop_value: + if (not entry.is_readable() and stop_on_invalid_pointers) or ( + iterator_guard_value is not None and i >= iterator_guard_value + ): + break + array.append(entry) + entry = context.object( + pointer_type, + layer_name=layer_name, + offset=entry.vol.offset + pointer_type.size, + ) + i += 1 + return array From 15a8af5c93bbef025fc0c0d3fcb69abc83854f9e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:10:37 +0200 Subject: [PATCH 042/128] use dynamically_sized_array_of_pointers in _get_sect_count --- .../symbols/linux/extensions/__init__.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 22eb46460..22d06baea 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -198,21 +198,17 @@ class module(generic.GenericIntelProcess): ) return 0 - entry = arr_offset.dereference() - symbol_table_name = self.get_symbol_table_name() - idx = 0 - while entry.is_readable(): - idx += 1 - entry = self._context.object( - symbol_table_name + constants.BANG + "pointer", - layer_name=self.vol.layer_name, - offset=entry.vol.offset - + self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ).size, - ) - - return idx + # We chose 1000 as an arbitrary guard value against + # extreme cases of smearing. + # See PR #1773 for more information. + bin_attrs_list = utility.dynamically_sized_array_of_pointers( + context=self._context, + layer_name=self.vol.layer_name, + symbol_table_name=self.get_symbol_table_name(), + array_offset=arr_offset.dereference().vol.offset, + iterator_guard_value=1000, + ) + return len(bin_attrs_list) @functools.cached_property def number_of_sections(self) -> int: From b20da9c7f553d9e2b9580727d37113a57dcd5d09 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 7 May 2025 16:26:47 +0200 Subject: [PATCH 043/128] correct type hinting --- volatility3/framework/objects/utility.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 9e9fef33a..afea99b0c 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -4,7 +4,7 @@ import re -from typing import Optional, Union +from typing import List, Optional, Union from volatility3.framework import interfaces, objects, constants, exceptions @@ -260,7 +260,7 @@ def dynamically_sized_array_of_pointers( stop_value: int = 0, iterator_guard_value: int = None, stop_on_invalid_pointers: bool = True, -) -> interfaces.objects.ObjectInterface: +) -> List[interfaces.objects.ObjectInterface]: """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). Args: @@ -272,6 +272,9 @@ def dynamically_sized_array_of_pointers( iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays that are known to have smeared entries before the end. + + Returns: + An array of pointer objects """ pointer_type = context.symbol_space.get_type( symbol_table_name + constants.BANG + "pointer" From 44969090e71f9d63a8d2a23ea94f5a61c8459923 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:41:39 +0200 Subject: [PATCH 044/128] lru_cache get_modules_memory_boundaries() --- volatility3/framework/symbols/linux/utilities/modules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 2b16ec6e0..bb360e0ba 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,6 +1,7 @@ import logging import warnings from abc import ABCMeta, abstractmethod +import functools from typing import ( Callable, Dict, @@ -71,7 +72,7 @@ class ModuleGathererInterface( class Modules(interfaces.configuration.VersionableInterface): """Kernel modules related utilities.""" - _version = (3, 0, 1) + _version = (3, 0, 2) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -311,6 +312,7 @@ class Modules(interfaces.configuration.VersionableInterface): return run_results @staticmethod + @functools.lru_cache def get_modules_memory_boundaries( context: interfaces.context.ContextInterface, vmlinux_module_name: str, From 6e67674c20c7e1e3084a0106eca842efe6e1101d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:46:25 +0200 Subject: [PATCH 045/128] add section address sanity check --- .../symbols/linux/utilities/module_extract.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e4f5705cf..1ff0f35fc 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -10,12 +10,14 @@ from typing import ( Dict, ) +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework from volatility3.framework import ( interfaces, exceptions, symbols, ) +from volatility3.framework.configuration import requirements from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -38,11 +40,21 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 1) + _version = (1, 0, 2) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 2), + ), + ] + @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -237,17 +249,32 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = ( + linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if ( + not modules_addr_min + <= kernel_layer.address_mask & section.address + < modules_addr_max + ): + continue name = section.get_name() original_sections[section.address] = name if not original_sections: return None - kernel = context.modules[vmlinux_name] - kernel_layer = context.layers[kernel.layer_name] - if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): sym_type = "Elf64_Sym" elf_hdr_type = "Elf64_Ehdr" From a5cc616d53f2db0d20e1dfff81e9d26ed2672b78 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:49:56 +0200 Subject: [PATCH 046/128] leverage the existing Array facility --- volatility3/framework/objects/utility.py | 65 +++++++++++------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index afea99b0c..be57f9ad7 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -3,11 +3,13 @@ # import re - -from typing import List, Optional, Union +import logging +from typing import Optional, Union from volatility3.framework import interfaces, objects, constants, exceptions +vollog = logging.getLogger(__name__) + def rol(value: int, count: int, max_bits: int = 64) -> int: """A rotate-left instruction in Python""" @@ -254,51 +256,46 @@ def array_of_pointers( def dynamically_sized_array_of_pointers( context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table_name: str, - array_offset: int, + array: interfaces.objects.ObjectInterface, + iterator_guard_value: int, + subtype: Union[str, interfaces.objects.Template], stop_value: int = 0, - iterator_guard_value: int = None, stop_on_invalid_pointers: bool = True, -) -> List[interfaces.objects.ObjectInterface]: +) -> interfaces.objects.ObjectInterface: """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). + Array iteration should always be performed with an arbitrary guard value as maximum size, + to prevent running forever in case something unexpected happens. Args: context: The context on which to operate. - layer_name: The layer on which the array should be constructed. - symbol_table_name: The symbol table to use to construct object types. - array_offset: The array offset within the layer, from which to start iterating. - stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + array: The object to cast to an array. iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. + subtype: The subtype of the array's pointers. + stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays that are known to have smeared entries before the end. Returns: An array of pointer objects """ - pointer_type = context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ) - entry = context.object( - pointer_type, - layer_name=layer_name, - offset=array_offset, - ) - i = 0 - array = [] - # entry and entry.vol.offset aren't the same thing, as - # - entry is naturally represented by the address that the pointer refers to; - # - entry.vol.offset is the offset at which the pointer lives. - while entry != stop_value: - if (not entry.is_readable() and stop_on_invalid_pointers) or ( - iterator_guard_value is not None and i >= iterator_guard_value + new_count = 0 + for entry in array_of_pointers( + array=array, count=iterator_guard_value, subtype=subtype, context=context + ): + # "entry" is naturally represented by the address that the pointer refers to + if (entry == stop_value) or ( + not entry.is_readable() and stop_on_invalid_pointers ): break - array.append(entry) - entry = context.object( - pointer_type, - layer_name=layer_name, - offset=entry.vol.offset + pointer_type.size, + new_count += 1 + else: + vollog.log( + constants.LOGLEVEL_V, + f"""Iterator guard value {iterator_guard_value} reached while iterating over array at offset {array.vol.offset:#x}.\ + This means that there is a bug (e.g. smearing) with this array, or that it may contain valid entries past the iterator guard value.""", ) - i += 1 - return array + + # Leverage the "Array" object instead of returning a Python list + return array_of_pointers( + array=array, count=new_count, subtype=subtype, context=context + ) From c8d90cffe43a7440b60322a2b19a16cdd7c5f22c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:52:18 +0200 Subject: [PATCH 047/128] adjust to the new NULL-terminated processing --- .../symbols/linux/extensions/__init__.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 22d06baea..953e3cb89 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -187,26 +187,31 @@ class module(generic.GenericIntelProcess): """ if grp.has_member("bin_attrs"): - arr_offset = grp.bin_attrs + arr_offset_ptr = grp.bin_attrs + arr_subtype = "bin_attribute" else: - arr_offset = grp.attrs + arr_offset_ptr = grp.attrs + arr_subtype = "attribute" - if not arr_offset.is_readable(): + if not arr_offset_ptr.is_readable(): vollog.log( constants.LOGLEVEL_V, f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}", ) return 0 - # We chose 1000 as an arbitrary guard value against - # extreme cases of smearing. + # We chose 100 as an arbitrary guard value to prevent + # looping forever in extreme cases, and because 100 is not expected + # to be a valid number of sections. If that still happens, + # Vol3 module processing will indicate that it is missing information + # with the following message: + # "Unable to reconstruct the ELF for module struct at" # See PR #1773 for more information. bin_attrs_list = utility.dynamically_sized_array_of_pointers( context=self._context, - layer_name=self.vol.layer_name, - symbol_table_name=self.get_symbol_table_name(), - array_offset=arr_offset.dereference().vol.offset, - iterator_guard_value=1000, + array=arr_offset_ptr.dereference(), + iterator_guard_value=100, + subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype, ) return len(bin_attrs_list) From 5d50ad2b2eab7fa54d3be917063ec9e649f25536 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 12:54:45 +0200 Subject: [PATCH 048/128] slight readability adjustment --- .../framework/symbols/linux/utilities/module_extract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 1ff0f35fc..161f2a167 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -263,9 +263,9 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): for index, section in enumerate(module.get_sections()): # Extra sanity check, to prevent OOM on heavily smeared samples at line # "size = next_address - address" - if ( - not modules_addr_min - <= kernel_layer.address_mask & section.address + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask < modules_addr_max ): continue From c89c7c3a06349f8e1fb518d9520e3784a7d4096d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 8 May 2025 14:03:28 +0200 Subject: [PATCH 049/128] rollback to 635237b to prevent circular import --- .../symbols/linux/utilities/module_extract.py | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 161f2a167..e4f5705cf 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -10,14 +10,12 @@ from typing import ( Dict, ) -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework from volatility3.framework import ( interfaces, exceptions, symbols, ) -from volatility3.framework.configuration import requirements from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -40,21 +38,11 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 2) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 2), - ), - ] - @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -249,32 +237,17 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ - kernel = context.modules[vmlinux_name] - kernel_layer = context.layers[kernel.layer_name] - modules_addr_min, modules_addr_max = ( - linux_utilities_modules.Modules.get_modules_memory_boundaries( - context, vmlinux_name - ) - ) - modules_addr_min &= kernel_layer.address_mask - modules_addr_max &= kernel_layer.address_mask - original_sections = {} for index, section in enumerate(module.get_sections()): - # Extra sanity check, to prevent OOM on heavily smeared samples at line - # "size = next_address - address" - if not ( - modules_addr_min - <= section.address & kernel_layer.address_mask - < modules_addr_max - ): - continue name = section.get_name() original_sections[section.address] = name if not original_sections: return None + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): sym_type = "Elf64_Sym" elf_hdr_type = "Elf64_Ehdr" From b8e12bec297a4f61b17ff10b0c27224ad45ec37d Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 20 Nov 2025 15:18:53 +0100 Subject: [PATCH 050/128] move ModuleExtract class in modules.py --- .../framework/plugins/linux/module_extract.py | 12 +- .../symbols/linux/utilities/module_extract.py | 8 +- .../symbols/linux/utilities/modules.py | 732 +++++++++++++++++- 3 files changed, 742 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index a8864281a..3b6b6f0e5 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -4,8 +4,8 @@ import logging from typing import List +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints @@ -17,7 +17,7 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.plugins.PluginInterface): """Recreates an ELF file from a specific address in the kernel""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) @@ -37,9 +37,9 @@ class ModuleExtract(interfaces.plugins.PluginInterface): optional=False, ), requirements.VersionRequirement( - name="linux_utilities_module_extract", - version=(1, 0, 0), - component=linux_utilities_module_extract.ModuleExtract, + name="linux_utilities_modules_module_extract", + version=(1, 0, 2), + component=linux_utilities_modules.ModuleExtract, ), ] @@ -58,7 +58,7 @@ class ModuleExtract(interfaces.plugins.PluginInterface): module = kernel.object(object_type="module", offset=base_address, absolute=True) - elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( + elf_data = linux_utilities_modules.ModuleExtract.extract_module( self.context, self.config["kernel"], module ) if not elf_data: diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e4f5705cf..3cc15085a 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -15,6 +15,7 @@ from volatility3.framework import ( interfaces, exceptions, symbols, + deprecation ) from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -34,7 +35,12 @@ vollog = logging.getLogger(__name__) # ModuleExtract.extract_module is the entry point and only visible method for plugins - +# See PR #1773 +@deprecation.renamed_class( + deprecated_class_name="ModuleExtract", + removal_date="2026-06-01", + message="volatility3.framework.symbols.linux.utilities.module_extract.ModuleExtract is to be deprecated. Use volatility3.framework.symbols.linux.utilities.modules.ModuleExtract instead.", +) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index bb360e0ba..42383ecae 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,7 +1,8 @@ import logging import warnings -from abc import ABCMeta, abstractmethod import functools +import struct +from abc import ABCMeta, abstractmethod from typing import ( Callable, Dict, @@ -16,7 +17,6 @@ from typing import ( Union, ) -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3 import framework from volatility3.framework import ( constants, @@ -25,12 +25,14 @@ from volatility3.framework import ( interfaces, objects, renderers, + symbols ) from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import tainting +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -782,6 +784,730 @@ class Modules(interfaces.configuration.VersionableInterface): yield name, value +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory +# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time +# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections +# There are also several other significant complications that we must deal with when trying to extract an LKM +# that can be analyzed with static analysis tools +# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections +# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense +# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory +# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, +# we create the .shstrtab based on the sections in memory and then glue it in as the final section + +# ModuleExtract.extract_module is the entry point and only visible method for plugins +class ModuleExtract(interfaces.configuration.VersionableInterface): + """Extracts Linux kernel module structures into an analyzable ELF file""" + + _version = (1, 0, 2) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules_modules", + component=Modules, + version=(3, 0, 2), + ), + ] + + @classmethod + def _find_section( + cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int + ) -> Optional[Tuple[str, int, int, int]]: + """ + Finds the section containing `sym_address` + """ + for name, index, address, size in section_lookups: + if address <= sym_address < address + size: + return name, index, address, size + + return None + + @classmethod + def _get_st_info_for_sym( + cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str + ) -> bytes: + """ + This is a helper function called from `_fix_sym_table` + + Calculates the `st_info` value for the given symbol + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + if sym.st_name > 0: + # Global symbol + bind = linux_constants.STB_GLOBAL + + if sym_address == 0: + sect_type = linux_constants.STT_NOTYPE + elif sect_name: + # rela = relocations + if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1: + sect_type = linux_constants.STT_FUNC + else: + sect_type = linux_constants.STT_OBJECT + + else: + # outside the module being extracted + sect_type = linux_constants.STT_NOTYPE + + else: + # Local symbol + bind = linux_constants.STB_LOCAL + sect_type = linux_constants.STT_SECTION + + # Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO + bind_bits = (bind << 4) & 0xF0 + type_bits = sect_type & 0xF + + st_info_int = (bind_bits | type_bits) & 0xFF + + return struct.pack("B", st_info_int) + + @classmethod + def _get_fixed_sym_fields( + cls, + st_fmt: str, + sym: interfaces.objects.ObjectInterface, + sections: List[Tuple[str, int, int, int]], + ) -> Tuple[str, int, int, int]: + """ + This is a helper function called from `_fix_sym_table` + + The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading + Static analysis tools do not understand these transformed values as they only make sense to the kernel loader + We must de-mangle these to have analysis tools understand symbols (a key aspect) + """ + # Start by trying to map a symbol to its section + sym_address = sym.st_value + sect_info = cls._find_section(sections, sym_address) + + if not sect_info: + # Symbol does not point into the module being extracted + sect_name, sect_index, sect_address = None, None, None + st_value_int = sym_address + else: + # relative address inside the section + sect_name, sect_index, sect_address, _ = sect_info + st_value_int = sym_address - sect_address + + # Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file + + # formatted to be written into the extracted file + st_value = struct.pack(st_fmt, st_value_int) + + # returns formatted to be written into the extracted file + st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name) + + # format to reference its section, if any + if sect_name: + st_shndx = struct.pack(" Optional[bytes]: + """ + This function implements the most painful part of the reconstruction + + The symbols in .symtab are broken/mangled during loading. + We need to normalize these for static analysis tools to understand the references. + Without proper symbols, analysis is pretty pointless and gets nowhere. + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + kernel = context.modules[vmlinux_name] + + # Gather the section information into a list + section_lookups: List[Tuple[str, int, int, int]] = [] + for index, (address, name) in enumerate(original_sections.items()): + # We are fixing symtab references... + if name == ".symtab": + continue + + size = section_sizes[address] + + # Add 1 to account for leading NULL section + section_lookups.append((name, index + 1, address, size)) + + # Build the array of symbols as they are in memory + sym_type = kernel.get_type(sym_type_name) + + symbols = kernel.object( + object_type="array", + subtype=sym_type, + offset=module.section_symtab, + count=module.num_symtab, + absolute=True, + ) + + # used to hold the new (fixed) symbol table + sym_table_data = b"" + + # build a correct/normalized Elf32_Sym or Elf64_Sym for each symbol + for sym in symbols: + # get the mangled fields' correct values + sect_name, st_value, st_info, st_shndx = cls._get_fixed_sym_fields( + st_fmt, sym, section_lookups + ) + + # these aren't mangled during loading + st_name = struct.pack(" Optional[Tuple[List, int, int]]: + """ + This function first parses the sections as maintained by the kernel + It then orders the sections by load address, and then gathers the data of each section + We also track the file_offset to correctly have alignment in the output file + + .symtab requires special handling as its so broken in memory as described in `_fix_sym_table` + The data of .strtab is read directly off the module structure and not its section + as the section from the original module has no meaning after loading as the kernel does not reference it. + """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = ( + Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} + for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask + < modules_addr_max + ): + continue + + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: + return None + + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + sym_type = "Elf64_Sym" + elf_hdr_type = "Elf64_Ehdr" + st_fmt = " Optional[bytes]: + """ + Creates a `bits` bit ELF header for the file based on recovered values + Called last as it needs information computed from the sections + + Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + if bits == 32: + fmt = " Optional[int]: + """ + This function makes a best effort to map common section names + to their attributes + """ + known_sections = { + ".note.gnu.build-id": linux_constants.SHT_NOTE, + ".text": linux_constants.SHT_PROGBITS, + ".init.text": linux_constants.SHT_PROGBITS, + ".exit.text": linux_constants.SHT_PROGBITS, + ".static_call.text": linux_constants.SHT_PROGBITS, + ".rodata": linux_constants.SHT_PROGBITS, + ".modinfo": linux_constants.SHT_PROGBITS, + "__param": linux_constants.SHT_PROGBITS, + ".data": linux_constants.SHT_PROGBITS, + ".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS, + ".comment": linux_constants.SHT_PROGBITS, + ".shstrtab": linux_constants.SHT_STRTAB, + ".symtab": linux_constants.SHT_SYMTAB, + ".strtab": linux_constants.SHT_STRTAB, + } + + sect_type_val = linux_constants.SHT_PROGBITS + + if section_name.find(".rela.") != -1: + sect_type_val = linux_constants.SHT_RELA + + elif section_name in known_sections: + sect_type_val = known_sections[section_name] + + return sect_type_val + + # all sections from memory are allocated (SHF_ALLOC) + # special check certain other sections to try and ensure extra flags are added where needed + @classmethod + def _calc_sect_flags(cls, name: str) -> int: + """ + Make a best effort to map common section names to their permissions + If we miss a section here, users of common static analysis tools can mark the + sections are writable or executable manually, but that becomes very cumbersome + and breaks initial analysis by the tool + """ + # All sections in memory are allocated (`A` in readelf -S) + flags = linux_constants.SHF_ALLOC + + if name in [".text", ".init.text", ".exit.text", ".static_call.text"]: + flags = flags | linux_constants.SHF_EXECINSTR + + elif name in [ + ".data", + ".init.data", + ".exit.data", + ".bss", + "__tracepoints", + ".data.once", + "_ftrace_events", + ".gnu.linkonce.this_module", + ]: + flags = flags | linux_constants.SHF_WRITE + + return flags + + @classmethod + def _calc_link( + cls, name: str, strtab_index: int, symtab_index: int, sect_type: int + ) -> int: + """ + Calculates the link value for a section + + The most important ones are symtab indexes for relocations + and to point the symbol table to the string tab + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return symtab_index + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + return strtab_index + + return 0 + + @classmethod + def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int: + """ + Calculates the entsize for relocation sections and the symbol table section + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return 24 + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + if bits == 32: + return 16 + else: + return 24 + + return 0 + + @classmethod + def _make_section_header( + cls, + bits: int, + name_index: int, + name: str, + address: int, + size: int, + file_offset: int, + strtab_index: int, + symtab_index: int, + ) -> Optional[bytes]: + """ + Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section + """ + if bits == 32: + fmt = " Optional[bytes]: + # Bail early if bad address sent in + try: + hasattr(module.sect_attrs, "nsections") + except exceptions.InvalidAddressException: + vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.") + return None + + # Gather sections + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result + + kernel = context.modules[vmlinux_name] + + # Figure out header sizes + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + header_type = "Elf64_Ehdr" + section_type = "Elf64_Shdr" + bits = 64 + else: + header_type = "Elf32_Ehdr" + section_type = "Elf32_Shdr" + bits = 32 + + header_type_size = kernel.get_type(header_type).size + section_type_size = kernel.get_type(section_type).size + + # Per Linux-spec, all LKMs must start with a null section header + # This buffer is used to hold the headers as they are built + sections_headers = b"\x00" * section_type_size + + # Holder of the data of the sections + sections_data = b"" + + # the .shstrtab section is "\x00" + section name for each section + # followed by a terminating null. + # It starts with the null string (\x00) + shstrtab_data = b"\x00" + + # Track where we end the sections and data to glue `.shstrtab` after + last_file_offset = None + last_sect_size = None + + # Start at 1 in the string table + name_index = 1 + + # Create the actual section headers + for index, (name, address, file_offset, section_data) in enumerate( + updated_sections + ): + # Make the section header + header_bytes = cls._make_section_header( + bits, + name_index, + name, + address, + len(section_data), + file_offset, + strtab_index, + symtab_index, + ) + if not header_bytes: + vollog.debug(f"make_section_header failed for section {name}") + return None + + # ndex into the string table + name_index += len(name) + 1 + + # concatenate the header and section bytes + sections_headers += header_bytes + sections_data += section_data + + # track where we are so .shstrtab goes into correct offset + last_file_offset = file_offset + last_sect_size = len(section_data) + + # append each section name to what will become .shstrtab + shstrtab_data += bytes(name, encoding="utf8") + b"\x00" + + # stick our own section reference string at end + # name_index points to the end of the last section string after the loop ends + shstrtab_data += b".shstrtab\x00" + + # create our .shstrtab section so sections have names + sections_headers += cls._make_section_header( + bits, + name_index, + ".shstrtab", + 0, + len(shstrtab_data), + last_file_offset + last_sect_size, + strtab_index, + symtab_index, + ) + + sections_data += shstrtab_data + + num_sections = len(updated_sections) + 1 + + header = cls._make_elf_header( + bits, + header_type_size + len(sections_data), + num_sections, + ) + + if not header: + vollog.error( + f"Hit error creating Elf header for module at {module.vol.offset:#x}" + ) + return None + + # Return our beautiful, hand-crafted, farm raised ELF file + return header + sections_data + sections_headers class ModuleGathererLsmod(ModuleGathererInterface): """ @@ -978,7 +1704,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): file_name = renderers.NotApplicableValue() if dump and open_implementation: - elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( + elf_data = ModuleExtract.extract_module( context, kernel_module_name, module ) if not elf_data: From e9ce095b2924eb7d143aa7da24b79ed69850097b Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Thu, 20 Nov 2025 15:57:56 +0100 Subject: [PATCH 051/128] black formatting --- .../symbols/linux/utilities/module_extract.py | 8 ++------ .../framework/symbols/linux/utilities/modules.py | 11 ++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 3cc15085a..5ec254368 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -11,12 +11,7 @@ from typing import ( ) from volatility3 import framework -from volatility3.framework import ( - interfaces, - exceptions, - symbols, - deprecation -) +from volatility3.framework import interfaces, exceptions, symbols, deprecation from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -35,6 +30,7 @@ vollog = logging.getLogger(__name__) # ModuleExtract.extract_module is the entry point and only visible method for plugins + # See PR #1773 @deprecation.renamed_class( deprecated_class_name="ModuleExtract", diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 42383ecae..adacc54e3 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -25,7 +25,7 @@ from volatility3.framework import ( interfaces, objects, renderers, - symbols + symbols, ) from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility @@ -784,6 +784,7 @@ class Modules(interfaces.configuration.VersionableInterface): yield name, value + # This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory # This extraction task is quite complicated as the Linux kernel discards the ELF header at load time # Due to this, to support static analysis, we must create an ELF header and proper file based on the sections @@ -795,6 +796,7 @@ class Modules(interfaces.configuration.VersionableInterface): # Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, # we create the .shstrtab based on the sections in memory and then glue it in as the final section + # ModuleExtract.extract_module is the entry point and only visible method for plugins class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" @@ -1010,10 +1012,8 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): """ kernel = context.modules[vmlinux_name] kernel_layer = context.layers[kernel.layer_name] - modules_addr_min, modules_addr_max = ( - Modules.get_modules_memory_boundaries( - context, vmlinux_name - ) + modules_addr_min, modules_addr_max = Modules.get_modules_memory_boundaries( + context, vmlinux_name ) modules_addr_min &= kernel_layer.address_mask modules_addr_max &= kernel_layer.address_mask @@ -1509,6 +1509,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): # Return our beautiful, hand-crafted, farm raised ELF file return header + sections_data + sections_headers + class ModuleGathererLsmod(ModuleGathererInterface): """ Gathers modules from the main kernel list From 32f37ee6323ea2f7ee206aafe7d7a0faf60ac36f Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 24 Nov 2025 17:34:31 +0100 Subject: [PATCH 052/128] remove hasattr check on bin_attribute --- .../symbols/linux/extensions/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 953e3cb89..68d1aae52 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -3227,18 +3227,13 @@ class bin_attribute(objects.StructType): """ Performs extraction of the bin_attribute name """ - if hasattr(self, "attr"): - try: - return utility.pointer_to_string( - self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE - ) - except exceptions.InvalidAddressException: - vollog.debug( - f"Invalid attr name for bin_attribute at {self.vol.offset:#x}" - ) - return None - - return None + try: + return utility.pointer_to_string( + self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + return None @property def address(self) -> int: From 8b132eafd15f885806028e522887e520d22c60bb Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 24 Nov 2025 17:57:37 +0100 Subject: [PATCH 053/128] extend _fix_sym_table's docstring --- volatility3/framework/symbols/linux/utilities/modules.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index adacc54e3..598e79b18 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -925,6 +925,15 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): module: extensions.module, ) -> Optional[bytes]: """ + Args: + context: The context on which to operate. + vmlinux_name: The name of the kernel module. + original_sections: Dict of module section addresses and names. + section_sizes: Dict of module section addresses and sizes. + sym_type_name: ELF symbol type name (should be one of "Elf64_Sym" or "Elf32_Sym"). + st_fmt: "struct"-like unpack format string (should be one of " Date: Mon, 1 Dec 2025 15:54:58 +0100 Subject: [PATCH 054/128] remove PTEs "large_page" attribute and re-order large_page check --- volatility3/framework/layers/intel.py | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 848580004..f750f3f2f 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -38,7 +38,7 @@ class Intel(linear.LinearlyMappedLayer): # NOTE: _maxphyaddr is MAXPHYADDR as defined in the Intel specs *NOT* the maximum physical address _maxphyaddr = 32 _maxvirtaddr = _maxphyaddr - _structure = [("page directory", 10, False), ("page table", 10, True)] + _structure = [("page directory", 10, True), ("page table", 10, False)] _direct_metadata = collections.ChainMap( {"architecture": "Intel32"}, {"mapped": True}, @@ -221,18 +221,6 @@ class Intel(linear.LinearlyMappedLayer): entry, "Page Fault at entry " + hex(entry) + " in table " + name, ) - # Check if we're a large page - if large_page and (entry & self._PAGE_PSE): - # Mask off the PAT bit - if entry & self._PAGE_PAT_LARGE: - entry -= self._PAGE_PAT_LARGE - # We're a large page, the rest is finished below - # If we want to implement PSE-36, it would need to be done here - break - # Figure out how much of the offset we should be using - start = position - position -= size - index = self._mask(page_address, start, position + 1) >> (position + 1) # Grab the base address of the table we'll be getting the next entry from base_address = self._mask( @@ -249,6 +237,11 @@ class Intel(linear.LinearlyMappedLayer): "Page Fault at entry " + hex(entry) + " in table " + name, ) + # Figure out how much of the offset we should be using + start = position + position -= size + index = self._mask(page_address, start, position + 1) >> (position + 1) + # Read the data for the next entry entry_data_start = index << self._index_shift entry_data = table[entry_data_start : entry_data_start + self._entry_size] @@ -262,6 +255,15 @@ class Intel(linear.LinearlyMappedLayer): # Read out the new entry from memory (entry,) = struct.unpack(self._entry_format, entry_data) + # Check if we're a large page + if large_page and (entry & self._PAGE_PSE): + # Mask off the PAT bit + if entry & self._PAGE_PAT_LARGE: + entry -= self._PAGE_PAT_LARGE + # We're a large page, the rest is finished below + # If we want to implement PSE-36, it would need to be done here + break + return entry, position @functools.lru_cache(maxsize=1025) @@ -429,7 +431,7 @@ class IntelPAE(Intel): _structure = [ ("page directory pointer", 2, False), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] _direct_metadata = collections.ChainMap({"pae": True}, Intel._direct_metadata) @@ -449,7 +451,7 @@ class Intel32e(Intel): ("page map layer 4", 9, False), ("page directory pointer", 9, True), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] From ae0f0ca440f878d7e7e56c163b6cb61eb58f19e5 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Mon, 1 Dec 2025 15:56:56 +0100 Subject: [PATCH 055/128] skip invalid blocks more efficiently --- volatility3/framework/layers/intel.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index f750f3f2f..4108d7231 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -269,9 +269,12 @@ class Intel(linear.LinearlyMappedLayer): @functools.lru_cache(maxsize=1025) def _get_valid_table(self, base_address: int) -> Optional[bytes]: """Extracts the table, validates it and returns it if it's valid.""" - table = self._context.layers.read( - self._base_layer, base_address, self.page_size - ) + try: + table = self._context.layers.read( + self._base_layer, base_address, self.page_size + ) + except exceptions.InvalidAddressException: + return None # If the table is entirely duplicates, then mark the whole table as bad if table == table[: self._entry_size] * self._entry_number: @@ -375,12 +378,19 @@ class Intel(linear.LinearlyMappedLayer): while length > 0: try: chunk_offset, page_size, layer_name = self._translate(offset) - chunk_size = min(page_size - (chunk_offset % page_size), length) + # Page align the chunk size value + chunk_size = min(page_size - (offset % page_size), length) if not self._context.layers[layer_name].is_valid( chunk_offset, chunk_size ): - raise exceptions.InvalidAddressException( - layer_name=layer_name, invalid_address=chunk_offset + # Virtual -> physical is contiguous in the chunk_size range. + # If we fail, we can jump directly to the end as we know all bytes in between + # aren't mapped (virtually and) physically anyway. + raise exceptions.PagedInvalidAddressException( + layer_name=layer_name, + invalid_address=chunk_offset, + entry=0, + invalid_bits=int(math.log2(chunk_size)), ) except ( exceptions.PagedInvalidAddressException, From f4e2f1391e25da8d0aa8e69da3bd1257bf2db2e1 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 11:22:27 +0100 Subject: [PATCH 056/128] refactor use of "invalid_bits" inside _mapping into a dedicated variable --- volatility3/framework/layers/intel.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 4108d7231..e6a3244cb 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -376,6 +376,7 @@ class Intel(linear.LinearlyMappedLayer): yield offset, length, mapped_offset, length, layer_name return None while length > 0: + skip_mask = None try: chunk_offset, page_size, layer_name = self._translate(offset) # Page align the chunk size value @@ -386,11 +387,9 @@ class Intel(linear.LinearlyMappedLayer): # Virtual -> physical is contiguous in the chunk_size range. # If we fail, we can jump directly to the end as we know all bytes in between # aren't mapped (virtually and) physically anyway. - raise exceptions.PagedInvalidAddressException( - layer_name=layer_name, - invalid_address=chunk_offset, - entry=0, - invalid_bits=int(math.log2(chunk_size)), + skip_mask = chunk_size - 1 + raise exceptions.InvalidAddressException( + layer_name=layer_name, invalid_address=chunk_offset ) except ( exceptions.PagedInvalidAddressException, @@ -398,12 +397,13 @@ class Intel(linear.LinearlyMappedLayer): ) as excp: if not ignore_errors: raise - # We can jump more if we know where the page fault failed - if isinstance(excp, exceptions.PagedInvalidAddressException): - mask = (1 << excp.invalid_bits) - 1 - else: - mask = (1 << self._page_size_in_bits) - 1 - length_diff = mask + 1 - (offset & mask) + if skip_mask is None: + # We can jump more if we know where the page fault occured + if isinstance(excp, exceptions.PagedInvalidAddressException): + skip_mask = (1 << excp.invalid_bits) - 1 + else: + skip_mask = (1 << self._page_size_in_bits) - 1 + length_diff = skip_mask + 1 - (offset & skip_mask) length -= length_diff offset += length_diff else: From 93d6282817045e4f0a0d8667a7cb2d4f52ae0c11 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 11:25:56 +0100 Subject: [PATCH 057/128] version bump: 2.27.0 -> 2.27.1 --- 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 7f71c277e..73eb5452e 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 = 27 # 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 89c5fc325a238e81294d3615c394b813ae2d1d89 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 17:34:25 +0100 Subject: [PATCH 058/128] move iterator_guard_value down the parameters list --- volatility3/framework/objects/utility.py | 2 +- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index be57f9ad7..05ad76d11 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -257,8 +257,8 @@ def array_of_pointers( def dynamically_sized_array_of_pointers( context: interfaces.context.ContextInterface, array: interfaces.objects.ObjectInterface, - iterator_guard_value: int, subtype: Union[str, interfaces.objects.Template], + iterator_guard_value: int, stop_value: int = 0, stop_on_invalid_pointers: bool = True, ) -> interfaces.objects.ObjectInterface: diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 68d1aae52..71ce82d82 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -210,8 +210,8 @@ class module(generic.GenericIntelProcess): bin_attrs_list = utility.dynamically_sized_array_of_pointers( context=self._context, array=arr_offset_ptr.dereference(), - iterator_guard_value=100, subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype, + iterator_guard_value=100, ) return len(bin_attrs_list) From d7103a22141c2f8d9e40c524cb56b5fefe032868 Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 17:41:58 +0100 Subject: [PATCH 059/128] ensure that _parse_sections returns a constant number of None on failure --- volatility3/framework/symbols/linux/utilities/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 598e79b18..63b25380a 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1134,7 +1134,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): vollog.debug( f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover." ) - return None, None, None + return None symtab_index = len(updated_sections) @@ -1145,7 +1145,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): vollog.debug( f"Did not find a .symtab section for module at {module.vol.offset:#x}. Cannot recover." ) - return None, None, None + return None return updated_sections, strtab_index, symtab_index From 0138aec419e5febdc13781b2ea6832bd151c9e1e Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Wed, 3 Dec 2025 17:43:39 +0100 Subject: [PATCH 060/128] manually iterate over the pointers to detect OOB locally --- volatility3/framework/objects/utility.py | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 05ad76d11..799639a67 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -279,14 +279,27 @@ def dynamically_sized_array_of_pointers( An array of pointer objects """ new_count = 0 - for entry in array_of_pointers( - array=array, count=iterator_guard_value, subtype=subtype, context=context - ): - # "entry" is naturally represented by the address that the pointer refers to - if (entry == stop_value) or ( - not entry.is_readable() and stop_on_invalid_pointers - ): + sym_table_name = array.get_symbol_table_name() + sym_table = context.symbol_space[sym_table_name] + ptr_size = sym_table.get_type("pointer").size + layer_name = array.vol.layer_name + + offset = array.vol.offset + entry = None + while entry != stop_value and new_count < iterator_guard_value: + try: + entry = context.object( + sym_table_name + constants.BANG + "pointer", + offset=offset, + layer_name=layer_name, + ) + except exceptions.InvalidAddressException: break + + if not entry.is_readable() and stop_on_invalid_pointers: + break + + offset += ptr_size new_count += 1 else: vollog.log( From 50f64f66d547fa8379d47ab8972be665347b4a4e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 16 Dec 2025 10:03:37 +0000 Subject: [PATCH 061/128] Document the reason for skipping entirely duplicated page tables --- volatility3/framework/layers/intel.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index e6a3244cb..380d0e49f 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -10,7 +10,7 @@ import struct from typing import Any, Dict, Iterable, List, Optional, Tuple from volatility3 import classproperty -from volatility3.framework import exceptions, interfaces, constants +from volatility3.framework import constants, exceptions, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.layers import linear @@ -276,7 +276,31 @@ class Intel(linear.LinearlyMappedLayer): except exceptions.InvalidAddressException: return None + #### # If the table is entirely duplicates, then mark the whole table as bad + # This is because Windows 10 onwards has a tendency to map unused pages as present + # This had the following consequences: + # - Used very litle physical memory + # - Exploded virtual memory + # - Causes *scan plugins to take multiple hours to complete even on small images + + # Previous versions of volatility would ignore a page during a scan when it matched + # the one directly preceding it in physical memory. + # This could trip if only two pages were identical and still required enumerating all + # the invalid pages (which itself was quite time consuming) + + # For this reason, volatility 3 shifted to looking at entire page tables (1,024 pages) + # and if all the pages mapped to the same place the table wouuld be skipped + # This could also be applied to the Directory level as well as the Table level, allowing + # Volatility to skip huge sections of virtual memory very efficiently, without missing + # any pages that were distinct within a particular page table (or directory). + + # In order to work at this level, the logic was moved out of the scanning component and + # directly into the layer logic itself. This does have the side effect of preventing + # entirely duplicated page tables from reporting as present, however, the trade off between + # Windows 10+ reduced scanning times (common amongst scan plugins) versus incorrectly reporting + # entire page tables of identically mapped repeating *valid* data (rare) was accepted in favour + # of the more common occurance. if table == table[: self._entry_size] * self._entry_number: return None return table From 105ee08d918be92e5abf551d4cbe820010e45982 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Dec 2025 14:09:51 +0000 Subject: [PATCH 062/128] Avoid trying to lookup child nodes that have been filtered Fixes issue1309. --- volatility3/cli/text_renderer.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d55201371..d400067af 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -10,8 +10,8 @@ import string import sys from functools import wraps from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union -from volatility3.cli import text_filter +from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints @@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( - [] - ) + final_output: List[ + Tuple[int, Dict[interfaces.renderers.Column, list[str]]] + ] = [] if not grid.populated: grid.populate(visitor, final_output) else: @@ -598,7 +598,8 @@ class JsonRenderer(CLIRenderer): if self.filter and self.filter.filter(line): return accumulator - if node.parent: + # Only add if the parent hasn't been filtered out + if node.parent and node.parent.path in acc_map: acc_map[node.parent.path]["__children"].append(node_dict) else: final_tree.append(node_dict) From 0089aefaf6a4b72729ff79ddaf2d0390ba24ddb7 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 28 Dec 2025 14:13:44 +0000 Subject: [PATCH 063/128] Reformat because ruff and black disagree --- volatility3/cli/text_renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d400067af..d00c00bf4 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[ - Tuple[int, Dict[interfaces.renderers.Column, list[str]]] - ] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( + [] + ) if not grid.populated: grid.populate(visitor, final_output) else: From 5aea7f92e94af4b04e811b73ceadb51cc9a355d3 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:00:13 +0300 Subject: [PATCH 064/128] process_spoofing plugin --- .../plugins/linux/process_spoofing.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 volatility3/framework/plugins/linux/process_spoofing.py diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py new file mode 100644 index 000000000..2546a3623 --- /dev/null +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -0,0 +1,253 @@ +# This file is Copyright 2025 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 pathlib import PurePosixPath +from typing import Optional, Tuple, Iterator + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +# https://github.com/SolitudePy/linux-mal +class ProcessSpoofing(plugins.PluginInterface): + """Detects process spoofing by comparing executable path to cmdline & comm fields""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + def _get_executable_path( + self, task: interfaces.objects.ObjectInterface + ) -> Optional[str]: + """ + Extract the executable path from task_struct.mm.exe_file + + Args: + task: task_struct object of the process + + Returns: + Executable path or None if not available + """ + try: + mm = task.mm + if not mm or not mm.is_readable(): + # Kernel threads doesn't have + return None + + exe_file = mm.exe_file + if not exe_file or not exe_file.is_readable(): + return None + + # Use LinuxUtilities.path_for_file to extract the path + exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + + return exe_path if exe_path else None + + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _get_cmdline_basename( + self, task: interfaces.objects.ObjectInterface + ) -> Optional[str]: + """ + Extract the command line arguments and return the basename of the first argument + + Args: + task: task_struct object of the process + + Returns: + Basename of the first command line argument or None if not available + """ + try: + mm = task.mm + if not mm or not mm.is_readable(): + return renderers.NotAvailableValue() + + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + + proc_layer = self.context.layers[proc_layer_name] + + # Read argv from userland + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start + + if not (0 < size_to_read <= 4096): + return None + + # Attempt to read command line arguments + try: + argv = proc_layer.read(start, size_to_read) + except exceptions.InvalidAddressException: + return None + + # Parse the arguments - they are null byte terminated + args_str = argv.decode(encoding="utf8", errors="replace") + args_list = args_str.split("\x00") + if args_list and args_list[0]: + basename = PurePosixPath(args_list[0]).name + return basename + else: + return None + + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _get_comm(self, task: interfaces.objects.ObjectInterface) -> Optional[str]: + """ + Extract the comm field from task_struct + + Args: + task: task_struct object of the process + + Returns: + Process name from comm field or None if not available + """ + try: + return utility.array_to_string(task.comm) + except (exceptions.InvalidAddressException, AttributeError): + return None + + def _extract_process_names( + self, task: interfaces.objects.ObjectInterface + ) -> Tuple[Optional[str], Optional[str], Optional[str]]: + """ + Extract all three process name sources for comparison + + Args: + task: task_struct object of the process + + Returns: + Tuple of (exe_path_basename, cmdline_basename, comm) + """ + exe_path = self._get_executable_path(task) + exe_basename = PurePosixPath(exe_path).name + cmdline_basename = self._get_cmdline_basename(task) + comm = self._get_comm(task) + + return exe_basename, cmdline_basename, comm + + def _detect_spoofing( + self, + exe_basename: Optional[str], + cmdline_basename: Optional[str], + comm: Optional[str], + ) -> Optional[list]: + """ + Analyze the three name sources to detect potential spoofing + + Args: + exe_basename: Basename from exe_file path + cmdline_basename: Basename from command line + comm: Name from comm field + + Returns: + notes: List of notes indicating potential spoofing, or None if no issues found + """ + notes = [] + + # Count how many name sources we have + available_sources = sum( + 1 for name in [exe_basename, cmdline_basename, comm] if name + ) + + if available_sources < 2: + return None + + if exe_basename != cmdline_basename: + notes.append( + f"'Potential cmdline spoofing: exe_file={exe_basename};cmdline={cmdline_basename}'" + ) + if exe_basename[:15] != comm: + notes.append( + f"'Potential comm spoofing: exe_file={exe_basename};comm={comm}'" + ) + return notes + + def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: + """ + Generate process spoofing detection results + + Args: + tasks: Iterator of task_struct objects + + Yields: + Tuple containing process information and spoofing analysis + """ + for task in tasks: + try: + pid = task.pid + ppid = task.get_parent_pid() + + exe_basename, cmdline_basename, comm = self._extract_process_names(task) + + notes = self._detect_spoofing(exe_basename, cmdline_basename, comm) + + exe_render = exe_basename if exe_basename else "N/A" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" + comm_render = comm if comm else "N/A" + + yield ( + 0, + ( + pid, + ppid, + exe_render, + cmdline_render, + comm_render, + "[" + ", ".join(notes) + "]" if notes else "OK", + ), + ) + + except Exception as e: + vollog.debug(f"Error processing task at {task.vol.offset:#x}: {e}") + continue + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("Exe_Basename", str), + ("Cmdline_Basename", str), + ("Comm", str), + ("Notes", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) From cf6fa422b479bdd9bc8e749e5a1d72a9b949d89b Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:10:44 +0300 Subject: [PATCH 065/128] cosmetics --- volatility3/framework/plugins/linux/process_spoofing.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py index 2546a3623..9d2e55acb 100644 --- a/volatility3/framework/plugins/linux/process_spoofing.py +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -60,14 +60,12 @@ class ProcessSpoofing(plugins.PluginInterface): try: mm = task.mm if not mm or not mm.is_readable(): - # Kernel threads doesn't have + # Kernel threads doesn't have mm return None exe_file = mm.exe_file if not exe_file or not exe_file.is_readable(): return None - - # Use LinuxUtilities.path_for_file to extract the path exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) return exe_path if exe_path else None @@ -97,15 +95,12 @@ class ProcessSpoofing(plugins.PluginInterface): return None proc_layer = self.context.layers[proc_layer_name] - - # Read argv from userland start = task.mm.arg_start size_to_read = task.mm.arg_end - task.mm.arg_start if not (0 < size_to_read <= 4096): return None - # Attempt to read command line arguments try: argv = proc_layer.read(start, size_to_read) except exceptions.InvalidAddressException: @@ -176,7 +171,7 @@ class ProcessSpoofing(plugins.PluginInterface): """ notes = [] - # Count how many name sources we have + # Skip kernel threads available_sources = sum( 1 for name in [exe_basename, cmdline_basename, comm] if name ) From 5c3fca35447247c0d59c814682ae95952fc1067a Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:10:11 +0300 Subject: [PATCH 066/128] added mechanism for deleted exe --- .../framework/plugins/linux/process_spoofing.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/process_spoofing.py index 9d2e55acb..451889029 100644 --- a/volatility3/framework/plugins/linux/process_spoofing.py +++ b/volatility3/framework/plugins/linux/process_spoofing.py @@ -21,7 +21,8 @@ class ProcessSpoofing(plugins.PluginInterface): """Detects process spoofing by comparing executable path to cmdline & comm fields""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 1, 0) + deleted = " (deleted)" @classmethod def get_requirements(cls): @@ -64,10 +65,17 @@ class ProcessSpoofing(plugins.PluginInterface): return None exe_file = mm.exe_file + if not exe_file or not exe_file.is_readable(): return None + + exe_inode = exe_file.dereference().f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + # If the inode link count is 0, the process image has been deleted + if exe_inode.i_nlink == 0: + exe_path += self.deleted + return exe_path if exe_path else None except (exceptions.InvalidAddressException, AttributeError): @@ -176,6 +184,11 @@ class ProcessSpoofing(plugins.PluginInterface): 1 for name in [exe_basename, cmdline_basename, comm] if name ) + is_deleted = exe_basename.endswith(self.deleted) + if is_deleted: + notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") + exe_basename = exe_basename[: len(self.deleted) * -1] + if available_sources < 2: return None From fd270a208d3ddb9c72cc047dd8b4901bb7f2e1b7 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:49:44 +0300 Subject: [PATCH 067/128] categorize as a malware plugin --- .../framework/plugins/linux/{ => malware}/process_spoofing.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename volatility3/framework/plugins/linux/{ => malware}/process_spoofing.py (100%) diff --git a/volatility3/framework/plugins/linux/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py similarity index 100% rename from volatility3/framework/plugins/linux/process_spoofing.py rename to volatility3/framework/plugins/linux/malware/process_spoofing.py From 15dc9ec4635170f70edcb8fb96c25251a3040d0c Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:05:10 +0300 Subject: [PATCH 068/128] Plugins: precise exception handling in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 451889029..b14dd208c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -237,8 +237,10 @@ class ProcessSpoofing(plugins.PluginInterface): ), ) - except Exception as e: - vollog.debug(f"Error processing task at {task.vol.offset:#x}: {e}") + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning( + f"Unable to process task PID {getattr(task, 'pid', 'unknown')} at {task.vol.offset:#x}: {e}" + ) continue def run(self): From d55cf9a9d0f518331e01d78fff88e2f06d0f9ef9 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:07:17 +0300 Subject: [PATCH 069/128] Plugins: remove exe_file dereference() --- .../framework/plugins/linux/malware/process_spoofing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b14dd208c..f32f80f8a 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -61,7 +61,7 @@ class ProcessSpoofing(plugins.PluginInterface): try: mm = task.mm if not mm or not mm.is_readable(): - # Kernel threads doesn't have mm + # Kernel threads don't have mm struct return None exe_file = mm.exe_file @@ -69,7 +69,7 @@ class ProcessSpoofing(plugins.PluginInterface): if not exe_file or not exe_file.is_readable(): return None - exe_inode = exe_file.dereference().f_path.dentry.d_inode + exe_inode = exe_file.f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) # If the inode link count is 0, the process image has been deleted From cc6aa2117b8b4700d52a6bdafc23c1f5cd14307b Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:14:40 +0300 Subject: [PATCH 070/128] Plugins: convert useful methods to classmethods --- .../plugins/linux/malware/process_spoofing.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index f32f80f8a..7302f2a71 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -46,13 +46,17 @@ class ProcessSpoofing(plugins.PluginInterface): ), ] - def _get_executable_path( - self, task: interfaces.objects.ObjectInterface + @classmethod + def get_executable_path( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ Extract the executable path from task_struct.mm.exe_file Args: + context: The context to operate on task: task_struct object of the process Returns: @@ -70,24 +74,28 @@ class ProcessSpoofing(plugins.PluginInterface): return None exe_inode = exe_file.f_path.dentry.d_inode - exe_path = linux.LinuxUtilities.path_for_file(self.context, task, exe_file) + exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) # If the inode link count is 0, the process image has been deleted if exe_inode.i_nlink == 0: - exe_path += self.deleted + exe_path += cls.deleted return exe_path if exe_path else None except (exceptions.InvalidAddressException, AttributeError): return None - def _get_cmdline_basename( - self, task: interfaces.objects.ObjectInterface + @classmethod + def get_cmdline_basename( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ Extract the command line arguments and return the basename of the first argument Args: + context: The context to operate on task: task_struct object of the process Returns: @@ -102,7 +110,7 @@ class ProcessSpoofing(plugins.PluginInterface): if proc_layer_name is None: return None - proc_layer = self.context.layers[proc_layer_name] + proc_layer = context.layers[proc_layer_name] start = task.mm.arg_start size_to_read = task.mm.arg_end - task.mm.arg_start @@ -126,7 +134,8 @@ class ProcessSpoofing(plugins.PluginInterface): except (exceptions.InvalidAddressException, AttributeError): return None - def _get_comm(self, task: interfaces.objects.ObjectInterface) -> Optional[str]: + @classmethod + def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: """ Extract the comm field from task_struct @@ -153,10 +162,10 @@ class ProcessSpoofing(plugins.PluginInterface): Returns: Tuple of (exe_path_basename, cmdline_basename, comm) """ - exe_path = self._get_executable_path(task) - exe_basename = PurePosixPath(exe_path).name - cmdline_basename = self._get_cmdline_basename(task) - comm = self._get_comm(task) + exe_path = self.get_executable_path(self.context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None + cmdline_basename = self.get_cmdline_basename(self.context, task) + comm = self.get_comm(task) return exe_basename, cmdline_basename, comm From 939034cd3344b5511ebbde43c1a3c50cdf15eb11 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:16:52 +0300 Subject: [PATCH 071/128] Plugins: consistent return values in process_spoofing --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 7302f2a71..01d4c1e2c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,7 +104,7 @@ class ProcessSpoofing(plugins.PluginInterface): try: mm = task.mm if not mm or not mm.is_readable(): - return renderers.NotAvailableValue() + return None proc_layer_name = task.add_process_layer() if proc_layer_name is None: From 83494796432fe9bd1348e5bfbd6fa3549b9e79ef Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:34:58 +0300 Subject: [PATCH 072/128] Plugins: process_spoofing log exceptions as debug --- .../plugins/linux/malware/process_spoofing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 01d4c1e2c..473bd7d07 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -82,7 +82,10 @@ class ProcessSpoofing(plugins.PluginInterface): return exe_path if exe_path else None - except (exceptions.InvalidAddressException, AttributeError): + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read executable path for task at {task.vol.offset:#x}: {e}" + ) return None @classmethod @@ -147,7 +150,8 @@ class ProcessSpoofing(plugins.PluginInterface): """ try: return utility.array_to_string(task.comm) - except (exceptions.InvalidAddressException, AttributeError): + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") return None def _extract_process_names( @@ -193,7 +197,7 @@ class ProcessSpoofing(plugins.PluginInterface): 1 for name in [exe_basename, cmdline_basename, comm] if name ) - is_deleted = exe_basename.endswith(self.deleted) + is_deleted = exe_basename and exe_basename.endswith(self.deleted) if is_deleted: notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") exe_basename = exe_basename[: len(self.deleted) * -1] From 1a92e749ffae2e6189aec920d4fd9178004ca046 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:39:05 +0300 Subject: [PATCH 073/128] Plugins: more precise exception handling in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 473bd7d07..b3cc37cc7 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -104,37 +104,36 @@ class ProcessSpoofing(plugins.PluginInterface): Returns: Basename of the first command line argument or None if not available """ + mm = task.mm + if not mm or not mm.is_readable(): + return None + + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + + proc_layer = context.layers[proc_layer_name] + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start + + if not (0 < size_to_read <= 4096): + return None + try: - mm = task.mm - if not mm or not mm.is_readable(): - return None + argv = proc_layer.read(start, size_to_read) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" + ) + return None - proc_layer_name = task.add_process_layer() - if proc_layer_name is None: - return None - - proc_layer = context.layers[proc_layer_name] - start = task.mm.arg_start - size_to_read = task.mm.arg_end - task.mm.arg_start - - if not (0 < size_to_read <= 4096): - return None - - try: - argv = proc_layer.read(start, size_to_read) - except exceptions.InvalidAddressException: - return None - - # Parse the arguments - they are null byte terminated - args_str = argv.decode(encoding="utf8", errors="replace") - args_list = args_str.split("\x00") - if args_list and args_list[0]: - basename = PurePosixPath(args_list[0]).name - return basename - else: - return None - - except (exceptions.InvalidAddressException, AttributeError): + # Parse the arguments - they are null byte terminated + args_str = argv.decode(encoding="utf8", errors="replace") + args_list = args_str.split("\x00") + if args_list and args_list[0]: + basename = PurePosixPath(args_list[0]).name + return basename + else: return None @classmethod From d11f50906ac92745d268329eb49fb1ba889c7528 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:58:17 +0300 Subject: [PATCH 074/128] Plugins: swap notes for boolean flags in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b3cc37cc7..7f2c9af3c 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -177,7 +177,7 @@ class ProcessSpoofing(plugins.PluginInterface): exe_basename: Optional[str], cmdline_basename: Optional[str], comm: Optional[str], - ) -> Optional[list]: + ) -> Tuple[bool, bool, bool]: """ Analyze the three name sources to detect potential spoofing @@ -187,32 +187,34 @@ class ProcessSpoofing(plugins.PluginInterface): comm: Name from comm field Returns: - notes: List of notes indicating potential spoofing, or None if no issues found + Tuple of (is_deleted, cmdline_spoofed, comm_spoofed) boolean flags """ - notes = [] - - # Skip kernel threads - available_sources = sum( - 1 for name in [exe_basename, cmdline_basename, comm] if name - ) - + # Check if process image has been deleted is_deleted = exe_basename and exe_basename.endswith(self.deleted) + + # Get clean basename for comparison (without " (deleted)" suffix) + clean_exe_basename = exe_basename if is_deleted: - notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'") - exe_basename = exe_basename[: len(self.deleted) * -1] + clean_exe_basename = exe_basename[: len(self.deleted) * -1] + # Skip kernel threads - need at least 2 sources for comparison + available_sources = sum( + 1 for name in [clean_exe_basename, cmdline_basename, comm] if name + ) if available_sources < 2: - return None + return False, False, False - if exe_basename != cmdline_basename: - notes.append( - f"'Potential cmdline spoofing: exe_file={exe_basename};cmdline={cmdline_basename}'" - ) - if exe_basename[:15] != comm: - notes.append( - f"'Potential comm spoofing: exe_file={exe_basename};comm={comm}'" - ) - return notes + # Check for cmdline spoofing + cmdline_spoofed = False + if clean_exe_basename and cmdline_basename: + cmdline_spoofed = clean_exe_basename != cmdline_basename + + # Check for comm spoofing (comm is truncated to 15 characters) + comm_spoofed = False + if clean_exe_basename and comm: + comm_spoofed = clean_exe_basename[:15] != comm + + return is_deleted, cmdline_spoofed, comm_spoofed def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: """ @@ -231,7 +233,9 @@ class ProcessSpoofing(plugins.PluginInterface): exe_basename, cmdline_basename, comm = self._extract_process_names(task) - notes = self._detect_spoofing(exe_basename, cmdline_basename, comm) + is_deleted, cmdline_spoofed, comm_spoofed = self._detect_spoofing( + exe_basename, cmdline_basename, comm + ) exe_render = exe_basename if exe_basename else "N/A" cmdline_render = cmdline_basename if cmdline_basename else "N/A" @@ -245,7 +249,9 @@ class ProcessSpoofing(plugins.PluginInterface): exe_render, cmdline_render, comm_render, - "[" + ", ".join(notes) + "]" if notes else "OK", + cmdline_spoofed, + comm_spoofed, + is_deleted, ), ) @@ -265,7 +271,9 @@ class ProcessSpoofing(plugins.PluginInterface): ("Exe_Basename", str), ("Cmdline_Basename", str), ("Comm", str), - ("Notes", str), + ("Cmdline_Spoofed", bool), + ("Comm_Spoofed", bool), + ("Deleted", bool), ], self._generator( pslist.PsList.list_tasks( From 5c7cf1115205673692193dc4292306781dad1bfa Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:59:42 +0300 Subject: [PATCH 075/128] black --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 7f2c9af3c..a38367bd1 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -191,7 +191,7 @@ class ProcessSpoofing(plugins.PluginInterface): """ # Check if process image has been deleted is_deleted = exe_basename and exe_basename.endswith(self.deleted) - + # Get clean basename for comparison (without " (deleted)" suffix) clean_exe_basename = exe_basename if is_deleted: From 41f964cfe12d3958c284e00a08860de94cbb6f52 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:16:59 +0300 Subject: [PATCH 076/128] Plugins: determine process exe deletion structurally --- .../plugins/linux/malware/process_spoofing.py | 109 ++++++++++-------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index a38367bd1..1c2b1a478 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -22,7 +22,6 @@ class ProcessSpoofing(plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (1, 1, 0) - deleted = " (deleted)" @classmethod def get_requirements(cls): @@ -51,7 +50,7 @@ class ProcessSpoofing(plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, task: interfaces.objects.ObjectInterface, - ) -> Optional[str]: + ) -> Tuple[Optional[str], bool]: """ Extract the executable path from task_struct.mm.exe_file @@ -60,33 +59,54 @@ class ProcessSpoofing(plugins.PluginInterface): task: task_struct object of the process Returns: - Executable path or None if not available + Tuple of (basename, is_deleted) or (None, False) if not available """ + is_deleted = False + try: mm = task.mm - if not mm or not mm.is_readable(): - # Kernel threads don't have mm struct - return None + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") + return None, is_deleted + if not mm or not mm.is_readable(): + # Kernel threads don't have mm struct + return None, is_deleted + + try: exe_file = mm.exe_file - - if not exe_file or not exe_file.is_readable(): - return None - - exe_inode = exe_file.f_path.dentry.d_inode - exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) - - # If the inode link count is 0, the process image has been deleted - if exe_inode.i_nlink == 0: - exe_path += cls.deleted - - return exe_path if exe_path else None - except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug( - f"Unable to read executable path for task at {task.vol.offset:#x}: {e}" + f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" ) - return None + return None, is_deleted + + if not exe_file or not exe_file.is_readable(): + return None, is_deleted + + try: + exe_inode = exe_file.f_path.dentry.d_inode + exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" + ) + return None, is_deleted + + if not exe_path: + return None, is_deleted + + try: + # Check if the inode link count is 0 (process image has been deleted) + is_deleted = exe_inode.i_nlink == 0 + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to check inode link count for task at {task.vol.offset:#x}: {e}" + ) + # Continue without deletion info - we still have the path + + basename = PurePosixPath(exe_path).name + return basename, is_deleted @classmethod def get_cmdline_basename( @@ -155,7 +175,7 @@ class ProcessSpoofing(plugins.PluginInterface): def _extract_process_names( self, task: interfaces.objects.ObjectInterface - ) -> Tuple[Optional[str], Optional[str], Optional[str]]: + ) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: """ Extract all three process name sources for comparison @@ -163,21 +183,20 @@ class ProcessSpoofing(plugins.PluginInterface): task: task_struct object of the process Returns: - Tuple of (exe_path_basename, cmdline_basename, comm) + Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_path = self.get_executable_path(self.context, task) - exe_basename = PurePosixPath(exe_path).name if exe_path else None + exe_basename, is_deleted = self.get_executable_path(self.context, task) cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) - return exe_basename, cmdline_basename, comm + return exe_basename, cmdline_basename, comm, is_deleted def _detect_spoofing( self, exe_basename: Optional[str], cmdline_basename: Optional[str], comm: Optional[str], - ) -> Tuple[bool, bool, bool]: + ) -> Tuple[bool, bool]: """ Analyze the three name sources to detect potential spoofing @@ -187,34 +206,26 @@ class ProcessSpoofing(plugins.PluginInterface): comm: Name from comm field Returns: - Tuple of (is_deleted, cmdline_spoofed, comm_spoofed) boolean flags + Tuple of (cmdline_spoofed, comm_spoofed) boolean flags """ - # Check if process image has been deleted - is_deleted = exe_basename and exe_basename.endswith(self.deleted) - - # Get clean basename for comparison (without " (deleted)" suffix) - clean_exe_basename = exe_basename - if is_deleted: - clean_exe_basename = exe_basename[: len(self.deleted) * -1] - # Skip kernel threads - need at least 2 sources for comparison available_sources = sum( - 1 for name in [clean_exe_basename, cmdline_basename, comm] if name + 1 for name in [exe_basename, cmdline_basename, comm] if name ) if available_sources < 2: - return False, False, False + return False, False # Check for cmdline spoofing cmdline_spoofed = False - if clean_exe_basename and cmdline_basename: - cmdline_spoofed = clean_exe_basename != cmdline_basename + if exe_basename and cmdline_basename: + cmdline_spoofed = exe_basename != cmdline_basename # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False - if clean_exe_basename and comm: - comm_spoofed = clean_exe_basename[:15] != comm + if exe_basename and comm: + comm_spoofed = exe_basename[:15] != comm - return is_deleted, cmdline_spoofed, comm_spoofed + return cmdline_spoofed, comm_spoofed def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: """ @@ -231,13 +242,19 @@ class ProcessSpoofing(plugins.PluginInterface): pid = task.pid ppid = task.get_parent_pid() - exe_basename, cmdline_basename, comm = self._extract_process_names(task) + exe_basename, cmdline_basename, comm, is_deleted = ( + self._extract_process_names(task) + ) - is_deleted, cmdline_spoofed, comm_spoofed = self._detect_spoofing( + cmdline_spoofed, comm_spoofed = self._detect_spoofing( exe_basename, cmdline_basename, comm ) + # Prepare display values exe_render = exe_basename if exe_basename else "N/A" + if is_deleted and exe_basename: + exe_render += " (deleted)" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -273,7 +290,7 @@ class ProcessSpoofing(plugins.PluginInterface): ("Comm", str), ("Cmdline_Spoofed", bool), ("Comm_Spoofed", bool), - ("Deleted", bool), + ("Exe_Deleted", bool), ], self._generator( pslist.PsList.list_tasks( From d384f62721aa578dfb6833768db2c043a6701447 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:19:23 +0300 Subject: [PATCH 077/128] Plugins: make get_executable_path more accurate in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 1c2b1a478..b9126c723 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -59,7 +59,7 @@ class ProcessSpoofing(plugins.PluginInterface): task: task_struct object of the process Returns: - Tuple of (basename, is_deleted) or (None, False) if not available + Tuple of (full_path, is_deleted) or (None, False) if not available """ is_deleted = False @@ -105,8 +105,7 @@ class ProcessSpoofing(plugins.PluginInterface): ) # Continue without deletion info - we still have the path - basename = PurePosixPath(exe_path).name - return basename, is_deleted + return exe_path, is_deleted @classmethod def get_cmdline_basename( @@ -185,7 +184,8 @@ class ProcessSpoofing(plugins.PluginInterface): Returns: Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_basename, is_deleted = self.get_executable_path(self.context, task) + exe_path, is_deleted = self.get_executable_path(self.context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) From 9686b5ae8bc8b76ac9f0f81e90e5b951dfe994a3 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:09:56 +0300 Subject: [PATCH 078/128] Plugins: utilize linuxutilities.path_for_file (deleted) logic solely --- .../plugins/linux/malware/process_spoofing.py | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index b9126c723..35cbd79d4 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -71,7 +71,7 @@ class ProcessSpoofing(plugins.PluginInterface): if not mm or not mm.is_readable(): # Kernel threads don't have mm struct - return None, is_deleted + return None try: exe_file = mm.exe_file @@ -79,33 +79,20 @@ class ProcessSpoofing(plugins.PluginInterface): vollog.debug( f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" ) - return None, is_deleted + return None if not exe_file or not exe_file.is_readable(): - return None, is_deleted + return None try: - exe_inode = exe_file.f_path.dentry.d_inode exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug( f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" ) - return None, is_deleted + return None - if not exe_path: - return None, is_deleted - - try: - # Check if the inode link count is 0 (process image has been deleted) - is_deleted = exe_inode.i_nlink == 0 - except (exceptions.InvalidAddressException, AttributeError) as e: - vollog.debug( - f"Unable to check inode link count for task at {task.vol.offset:#x}: {e}" - ) - # Continue without deletion info - we still have the path - - return exe_path, is_deleted + return exe_path @classmethod def get_cmdline_basename( @@ -184,12 +171,12 @@ class ProcessSpoofing(plugins.PluginInterface): Returns: Tuple of (exe_basename, cmdline_basename, comm, is_deleted) """ - exe_path, is_deleted = self.get_executable_path(self.context, task) + exe_path = self.get_executable_path(self.context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) - return exe_basename, cmdline_basename, comm, is_deleted + return exe_path, exe_basename, cmdline_basename, comm def _detect_spoofing( self, @@ -242,7 +229,7 @@ class ProcessSpoofing(plugins.PluginInterface): pid = task.pid ppid = task.get_parent_pid() - exe_basename, cmdline_basename, comm, is_deleted = ( + exe_path, exe_basename, cmdline_basename, comm = ( self._extract_process_names(task) ) @@ -250,10 +237,7 @@ class ProcessSpoofing(plugins.PluginInterface): exe_basename, cmdline_basename, comm ) - # Prepare display values - exe_render = exe_basename if exe_basename else "N/A" - if is_deleted and exe_basename: - exe_render += " (deleted)" + is_deleted = exe_path.endswith(" (deleted)") if exe_path else False cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -263,7 +247,7 @@ class ProcessSpoofing(plugins.PluginInterface): ( pid, ppid, - exe_render, + exe_path, cmdline_render, comm_render, cmdline_spoofed, @@ -285,7 +269,7 @@ class ProcessSpoofing(plugins.PluginInterface): [ ("PID", int), ("PPID", int), - ("Exe_Basename", str), + ("Exe_Path", str), ("Cmdline_Basename", str), ("Comm", str), ("Cmdline_Spoofed", bool), From 1359a11f24cbb6bf5d09a31924d52cb2c920caa6 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:13:57 +0300 Subject: [PATCH 079/128] Plugins: truncate deleted to check spoofing in process_spoofing --- .../framework/plugins/linux/malware/process_spoofing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 35cbd79d4..a0cee5e4b 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -169,10 +169,12 @@ class ProcessSpoofing(plugins.PluginInterface): task: task_struct object of the process Returns: - Tuple of (exe_basename, cmdline_basename, comm, is_deleted) + Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ exe_path = self.get_executable_path(self.context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None + if exe_basename.endswith(" (deleted)"): + exe_basename = exe_basename[:-len(" (deleted)")] cmdline_basename = self.get_cmdline_basename(self.context, task) comm = self.get_comm(task) @@ -210,6 +212,7 @@ class ProcessSpoofing(plugins.PluginInterface): # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False if exe_basename and comm: + print(exe_basename, comm) comm_spoofed = exe_basename[:15] != comm return cmdline_spoofed, comm_spoofed From cbe5b46b8e542c4407af8389355f8c01dc15430f Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:21:19 +0300 Subject: [PATCH 080/128] Plugins: process_spoofing change extract_process_names to classmethod --- .../plugins/linux/malware/process_spoofing.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index a0cee5e4b..fafca4da1 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -50,7 +50,7 @@ class ProcessSpoofing(plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, task: interfaces.objects.ObjectInterface, - ) -> Tuple[Optional[str], bool]: + ) -> Optional[str]: """ Extract the executable path from task_struct.mm.exe_file @@ -59,15 +59,14 @@ class ProcessSpoofing(plugins.PluginInterface): task: task_struct object of the process Returns: - Tuple of (full_path, is_deleted) or (None, False) if not available + Returns executable path or None if not available """ - is_deleted = False try: mm = task.mm except (exceptions.InvalidAddressException, AttributeError) as e: vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") - return None, is_deleted + return None if not mm or not mm.is_readable(): # Kernel threads don't have mm struct @@ -159,24 +158,24 @@ class ProcessSpoofing(plugins.PluginInterface): vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") return None - def _extract_process_names( - self, task: interfaces.objects.ObjectInterface - ) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: + @classmethod + def extract_process_names( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: """ - Extract all three process name sources for comparison - - Args: - task: task_struct object of the process - + Extract all process name sources for comparison + Returns: Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ - exe_path = self.get_executable_path(self.context, task) + exe_path = cls.get_executable_path(context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None - if exe_basename.endswith(" (deleted)"): + if exe_basename and exe_basename.endswith(" (deleted)"): exe_basename = exe_basename[:-len(" (deleted)")] - cmdline_basename = self.get_cmdline_basename(self.context, task) - comm = self.get_comm(task) + cmdline_basename = cls.get_cmdline_basename(context, task) + comm = cls.get_comm(task) return exe_path, exe_basename, cmdline_basename, comm @@ -212,7 +211,6 @@ class ProcessSpoofing(plugins.PluginInterface): # Check for comm spoofing (comm is truncated to 15 characters) comm_spoofed = False if exe_basename and comm: - print(exe_basename, comm) comm_spoofed = exe_basename[:15] != comm return cmdline_spoofed, comm_spoofed @@ -233,7 +231,7 @@ class ProcessSpoofing(plugins.PluginInterface): ppid = task.get_parent_pid() exe_path, exe_basename, cmdline_basename, comm = ( - self._extract_process_names(task) + self.extract_process_names(self.context, task) ) cmdline_spoofed, comm_spoofed = self._detect_spoofing( From c3ddc094cbcccda4fd70fdbd4375e2249d470b73 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:27:46 +0300 Subject: [PATCH 081/128] Plugins: process spoofing handle none values and set more classmethods --- .../framework/plugins/linux/malware/process_spoofing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index fafca4da1..428a79bc7 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -240,6 +240,8 @@ class ProcessSpoofing(plugins.PluginInterface): is_deleted = exe_path.endswith(" (deleted)") if exe_path else False + # Convert None values to strings for TreeGrid compatibility + exe_path_render = exe_path if exe_path else "N/A" cmdline_render = cmdline_basename if cmdline_basename else "N/A" comm_render = comm if comm else "N/A" @@ -248,7 +250,7 @@ class ProcessSpoofing(plugins.PluginInterface): ( pid, ppid, - exe_path, + exe_path_render, cmdline_render, comm_render, cmdline_spoofed, From 4cb326fb6ed8cefb79fb80b20014936757c52c32 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:28:44 +0300 Subject: [PATCH 082/128] black and ruff --- .../framework/plugins/linux/malware/process_spoofing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 428a79bc7..1048378b9 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -166,14 +166,14 @@ class ProcessSpoofing(plugins.PluginInterface): ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: """ Extract all process name sources for comparison - + Returns: Tuple of (exe_path, exe_basename, cmdline_basename, comm) """ exe_path = cls.get_executable_path(context, task) exe_basename = PurePosixPath(exe_path).name if exe_path else None if exe_basename and exe_basename.endswith(" (deleted)"): - exe_basename = exe_basename[:-len(" (deleted)")] + exe_basename = exe_basename[: -len(" (deleted)")] cmdline_basename = cls.get_cmdline_basename(context, task) comm = cls.get_comm(task) From 9c5950e5957f7e6980cc41708830aa74bdb47980 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:29:15 +0300 Subject: [PATCH 083/128] Plugins: process_spoofing bump required_framework_version --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 1048378b9..941f1495e 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -20,7 +20,7 @@ vollog = logging.getLogger(__name__) class ProcessSpoofing(plugins.PluginInterface): """Detects process spoofing by comparing executable path to cmdline & comm fields""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 27, 0) _version = (1, 1, 0) @classmethod From d13de25b46c05abf19c2770d2cd6bcc13985ecdc Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:26:09 +0200 Subject: [PATCH 084/128] Constants: add MAX_ARG_STRLEN linux constant --- volatility3/framework/constants/linux/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 72440e23e..793ef11e8 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -9,6 +9,10 @@ Linux-specific values that aren't found in debug symbols import enum from dataclasses import dataclass +# Exec argument limits +# Ref: include/uapi/linux/binfmts.h (linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940) +MAX_ARG_STRLEN = 32 * 4096 + KERNEL_NAME = "__kernel__" """The value hard coded from the Linux Kernel (hence not extracted from the layer itself)""" From a2e033dfc8773a5583136752d06fdbe49db3304f Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:18:48 +0000 Subject: [PATCH 085/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 4f16920cf..1709dfbfc 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -339,7 +339,7 @@ class Sockscan(plugins.PluginInterface): ) return None - def _generator(self, symbol_table_name: str): + def _generator(self, kernel_module_name: str): """Scans for sockets. Each row represents a kernel socket. Args: From 5205d2cee0212b0662007152d363393dbf0ed424 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:19:08 +0000 Subject: [PATCH 086/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 1709dfbfc..b15d7027b 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -343,7 +343,7 @@ class Sockscan(plugins.PluginInterface): """Scans for sockets. Each row represents a kernel socket. Args: - symbol_table_name: The name of the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate Yields: addr: Physical offset From c7510f7e00c388c0c316d281ae78b0d4b03b0aff Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:19:37 +0000 Subject: [PATCH 087/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index b15d7027b..02e673d84 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -358,7 +358,7 @@ class Sockscan(plugins.PluginInterface): """ # get vmlinux module from context in order to build objects and read symbols - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] # get the memory layer that is to be scanned. memory_layer_name = self._find_memory_layer_name(symbol_table_name) From be11441ef0bd528f8dafc70f174682078cd4ef2d Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:19:50 +0000 Subject: [PATCH 088/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 02e673d84..d30d819aa 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -361,7 +361,7 @@ class Sockscan(plugins.PluginInterface): vmlinux = self.context.modules[kernel_module_name] # get the memory layer that is to be scanned. - memory_layer_name = self._find_memory_layer_name(symbol_table_name) + memory_layer_name = self._find_memory_layer_name(kernel_module_name) memory_layer = self.context.layers[memory_layer_name] # use the init process to build a sock handler From 7ba46943bbd1faa130f7ac3730f5c5b440e64c1d Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:19:59 +0000 Subject: [PATCH 089/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index d30d819aa..c6aaedf12 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -411,7 +411,7 @@ class Sockscan(plugins.PluginInterface): # if match is from file_ops_needles attempt to walk from file object to the sock if match in file_ops_needles: psock = self._walk_file_ops_needles( - symbol_table_name, memory_layer_name, needle_addr, f_op_offset + kernel_module_name, memory_layer_name, needle_addr, f_op_offset ) if psock is not None and sock_physical_addr not in seen_sock_physical_addr: From 3872133f5d99cfca014ca4314b4311a922f0708f Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:20:09 +0000 Subject: [PATCH 090/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index c6aaedf12..caeb3fbf2 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -379,7 +379,7 @@ class Sockscan(plugins.PluginInterface): # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( - symbol_table_name + kernel_module_name ) # TODO Method 3 - find sock by sk_error_report symbols From 4fbd54361a43c1bced645adfbcf5dd187d7d28ca Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:20:20 +0000 Subject: [PATCH 091/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index caeb3fbf2..3cf7277b8 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -375,7 +375,7 @@ class Sockscan(plugins.PluginInterface): progress_callback = self._progress_callback # Method 1 - find sockets by file operations, then follow pointers to sockets - file_ops_needles, f_op_offset = self._find_file_ops_needles(symbol_table_name) + file_ops_needles, f_op_offset = self._find_file_ops_needles(kernel_module_name) # Method 2 - find sockets by socket destructor directly inside sock objects socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( From 35d24d8d1794c5f0f5e322c6104afe6dd51cbd9f Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:20:30 +0000 Subject: [PATCH 092/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 3cf7277b8..7a230df78 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -193,7 +193,7 @@ class Sockscan(plugins.PluginInterface): def _walk_file_ops_needles( self, symbol_table_name, memory_layer_name, needle_addr, f_op_offset ): - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] try: # create file in the memory_layer, the native layer matches the # kernel so that pointers can be followed From 13aa0dd2dc2aa80b80928933481c8f1a68570852 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:20:44 +0000 Subject: [PATCH 093/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 7a230df78..cd2998ce4 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -368,7 +368,7 @@ class Sockscan(plugins.PluginInterface): # TODO: look into options so that sockstat.SockHandlers so that process_sock can # be used without a task object. init_task = vmlinux.object_from_symbol(symbol_name="init_task") - sock_handler = sockstat.SockHandlers(self.context, symbol_table_name, init_task) + sock_handler = sockstat.SockHandlers(self.context, kernel_module_name, init_task) # get progress_callback in order to use this in the scanners. # TODO: perhaps add more detail to progress, showing method in progress and number of hits found From da6a1e54443206fac42e7624a9c3180ed04f7406 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:20:57 +0000 Subject: [PATCH 094/128] Update volatility3/framework/plugins/linux/sockscan.py Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index cd2998ce4..38abcbf0a 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -57,7 +57,7 @@ class Sockscan(plugins.PluginInterface): ] def _canonicalize_symbol_addrs( - self, symbol_table_name: str, symbol_names: List[str] + self, kernel_module_name: str, symbol_names: List[str] ) -> Set[bytes]: """Takes a list of symbol names and converts the address of each to the bytes as they would appear in memory so that they can be scanned for. From 4369935be47669f17758015d5b0127be981582d4 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:21:19 +0000 Subject: [PATCH 095/128] Apply suggestion from @ikelos Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 38abcbf0a..3091fba4d 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -65,7 +65,7 @@ class Sockscan(plugins.PluginInterface): Symbols that cannot be found are ignored and not included in the results. Args: - symbol_table_name: The name of the kernel module on which to operate + kernel_module_name: The name of the kernel module on which to operate symbol_names: A list of symbol names to be looked up Returns: From 55c0b9b0a84d60e21973f6aa3977ae4c0f7e4e8e Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:21:32 +0000 Subject: [PATCH 096/128] Apply suggestion from @ikelos Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 3091fba4d..fc65976de 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -72,7 +72,7 @@ class Sockscan(plugins.PluginInterface): A set of bytes which are the packed addresses. """ # get vmlinux module from context in order to build objects and read symbols - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] # get kernel layer from context so that it's dependencies can be found, and therefore scanned. # kernel layer will be virtual and built ontop of a physical layer. From c47ab6ba1176ca5da3cf285d952f8df6dd3fd5a9 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:21:47 +0000 Subject: [PATCH 097/128] Apply suggestion from @ikelos Co-authored-by: ikelos --- volatility3/framework/plugins/linux/sockscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index fc65976de..d99157b3b 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -112,7 +112,7 @@ class Sockscan(plugins.PluginInterface): return packed_needles - def _find_memory_layer_name(self, symbol_table_name: str): + def _find_memory_layer_name(self, kernel_module_name: str): """Find the memory layer below the kernel. Only returns a single layer, and will warn the user if multiple layers are found. From f11347739b065d54a4c72daf32ce31d0c783f501 Mon Sep 17 00:00:00 2001 From: Eve <120014766+eve-mem@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:22:48 +0000 Subject: [PATCH 098/128] Apply suggestions from code review Co-authored-by: ikelos --- .../framework/plugins/linux/sockscan.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index d99157b3b..116dfa4ae 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -117,14 +117,14 @@ class Sockscan(plugins.PluginInterface): and will warn the user if multiple layers are found. Args: - symbol_table_name: The name of the kernel module on which to operate. + kernel_module_name: The name of the kernel module on which to operate. Returns: memory_layer_name: The name of the layer below the kernel to be scanned. """ # get vmlinux module from context in order to build objects and read symbols - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] # get kernel layer from context so that it's dependencies can be found, and therefore scanned. # kernel layer will be virtual and built ontop of a physical layer. @@ -150,16 +150,16 @@ class Sockscan(plugins.PluginInterface): return memory_layer_name - def _find_file_ops_needles(self, symbol_table_name: str): + def _find_file_ops_needles(self, kernel_module_name: str): # get vmlinux module from context in order to read symbols - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] file_ops_symbol_names = [ "socket_file_ops", "sockfs_dentry_operations", ] file_ops_needles = self._canonicalize_symbol_addrs( - symbol_table_name, file_ops_symbol_names + kernel_module_name, file_ops_symbol_names ) # get file struct to find the offset to the f_op pointer # this is so that the file object can be created at the correct offset, @@ -168,9 +168,9 @@ class Sockscan(plugins.PluginInterface): return (file_ops_needles, f_op_offset) - def _find_sk_destruct_needles(self, symbol_table_name: str): + def _find_sk_destruct_needles(self, kernel_module_name: str): # get vmlinux module from context in order to read symbols - vmlinux = self.context.modules[symbol_table_name] + vmlinux = self.context.modules[kernel_module_name] socket_destructor_symbol_names = [ "sock_def_destruct", @@ -180,7 +180,7 @@ class Sockscan(plugins.PluginInterface): "inet_sock_destruct", ] socket_destructor_needles = self._canonicalize_symbol_addrs( - symbol_table_name, socket_destructor_symbol_names + kernel_module_name, socket_destructor_symbol_names ) # get sock struct to find the offset to the sk_destruct pointer # this is so that the sock object can be created at the correct offset, @@ -191,7 +191,7 @@ class Sockscan(plugins.PluginInterface): return (socket_destructor_needles, sk_destruct_offset) def _walk_file_ops_needles( - self, symbol_table_name, memory_layer_name, needle_addr, f_op_offset + self, kernel_module_name, memory_layer_name, needle_addr, f_op_offset ): vmlinux = self.context.modules[kernel_module_name] try: From 6aa552cd03d14837b593c34bf9a4b99a4f800bae Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 31 Dec 2025 12:25:22 +0000 Subject: [PATCH 099/128] Fix black formatting in sockscan --- volatility3/framework/plugins/linux/sockscan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 116dfa4ae..984c503b8 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -368,7 +368,9 @@ class Sockscan(plugins.PluginInterface): # TODO: look into options so that sockstat.SockHandlers so that process_sock can # be used without a task object. init_task = vmlinux.object_from_symbol(symbol_name="init_task") - sock_handler = sockstat.SockHandlers(self.context, kernel_module_name, init_task) + sock_handler = sockstat.SockHandlers( + self.context, kernel_module_name, init_task + ) # get progress_callback in order to use this in the scanners. # TODO: perhaps add more detail to progress, showing method in progress and number of hits found From 66ec4f43723af757630f3ca3293cf47825f7ad7f Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 31 Dec 2025 12:49:45 +0000 Subject: [PATCH 100/128] Linux: update sockstat docstring for _walk_file_ops_needles. Rename physical_memory_layer_name for clarity --- .../framework/plugins/linux/sockscan.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 984c503b8..0056a1925 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -191,8 +191,28 @@ class Sockscan(plugins.PluginInterface): return (socket_destructor_needles, sk_destruct_offset) def _walk_file_ops_needles( - self, kernel_module_name, memory_layer_name, needle_addr, f_op_offset + self, + kernel_module_name: str, + physical_memory_layer_name: str, + needle_addr: int, + f_op_offset: int, ): + """ + This method attempts to walk from the f_op member of files to the + corresponding socket. If sucessful the socket object is created on the + memory layer and returned. + + Args: + kernel_module_name (str): The name of the kernel module from which, + to retrieve the file operations. + physical_memory_layer_name (str): The name of the physical memory layer that was scanned + needle_addr: The address of the needle that was found during the scanning + f_op_offset: The offset to the f_op member of the file type + + Returns: + psock: The sock object that was built on the memory layer + """ + vmlinux = self.context.modules[kernel_module_name] try: # create file in the memory_layer, the native layer matches the @@ -201,7 +221,7 @@ class Sockscan(plugins.PluginInterface): pfile = self.context.object( vmlinux.symbol_table_name + constants.BANG + "file", offset=sock_physical_addr, - layer_name=memory_layer_name, + layer_name=physical_memory_layer_name, native_layer_name=vmlinux.layer_name, ) dentry = pfile.get_dentry() @@ -248,7 +268,7 @@ class Sockscan(plugins.PluginInterface): psock = self.context.object( vmlinux.symbol_table_name + constants.BANG + "sock", offset=physical_sock_offset, - layer_name=memory_layer_name, + layer_name=physical_memory_layer_name, native_layer_name=vmlinux.layer_name, ) From 94f989be7ab0fe7704ca69d134b29f27d8a3a84b Mon Sep 17 00:00:00 2001 From: eve Date: Wed, 31 Dec 2025 12:54:41 +0000 Subject: [PATCH 101/128] Linux: update sockscan to add docstring to _find_file_ops_needles --- volatility3/framework/plugins/linux/sockscan.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py index 0056a1925..9288d85f0 100644 --- a/volatility3/framework/plugins/linux/sockscan.py +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -151,6 +151,16 @@ class Sockscan(plugins.PluginInterface): return memory_layer_name def _find_file_ops_needles(self, kernel_module_name: str): + """Retrieves socket file symbols and the offset to the 'f_op' pointer. + + Args: + kernel_module_name (str): The name of the kernel module to search. + + Returns: + Tuple[List[int], int]: A list of file symbol addresses and, + the offset to the 'f_op' pointer. + """ + # get vmlinux module from context in order to read symbols vmlinux = self.context.modules[kernel_module_name] From 2478c1398eb5b0afebc78d237a710d6d2aa760f1 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:06:50 +0200 Subject: [PATCH 102/128] Plugins: get first argument using utility.address_to_string in process_spoofing --- .../plugins/linux/malware/process_spoofing.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 941f1495e..64f3274e9 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -7,6 +7,7 @@ from pathlib import PurePosixPath from typing import Optional, Tuple, Iterator from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.constants import linux as linux_constants from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility @@ -117,30 +118,35 @@ class ProcessSpoofing(plugins.PluginInterface): if proc_layer_name is None: return None - proc_layer = context.layers[proc_layer_name] start = task.mm.arg_start size_to_read = task.mm.arg_end - task.mm.arg_start - if not (0 < size_to_read <= 4096): + if size_to_read <= 0: return None + read_length = min(size_to_read, linux_constants.MAX_ARG_STRLEN) + try: - argv = proc_layer.read(start, size_to_read) + cmdline = utility.address_to_string( + context=context, + layer_name=proc_layer_name, + address=start, + count=read_length, + errors="replace", + encoding="utf-8" + ) except exceptions.InvalidAddressException as e: vollog.debug( f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" ) return None - # Parse the arguments - they are null byte terminated - args_str = argv.decode(encoding="utf8", errors="replace") - args_list = args_str.split("\x00") - if args_list and args_list[0]: - basename = PurePosixPath(args_list[0]).name - return basename - else: + if not cmdline: return None + basename = PurePosixPath(cmdline).name + return basename if basename else None + @classmethod def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: """ From 088a64379cddf40f6d7f8c4465724233338ba111 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:13:01 +0200 Subject: [PATCH 103/128] black --- volatility3/framework/plugins/linux/malware/process_spoofing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 64f3274e9..0d64861b2 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -133,7 +133,7 @@ class ProcessSpoofing(plugins.PluginInterface): address=start, count=read_length, errors="replace", - encoding="utf-8" + encoding="utf-8", ) except exceptions.InvalidAddressException as e: vollog.debug( From e8087dd68d77391ca6a201abb9a64a558e104a13 Mon Sep 17 00:00:00 2001 From: SolitudePy <47316655+SolitudePy@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:23:05 +0200 Subject: [PATCH 104/128] Plugins: process_spoofing put reference in docstring --- .../plugins/linux/malware/process_spoofing.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py index 0d64861b2..493a1de9a 100644 --- a/volatility3/framework/plugins/linux/malware/process_spoofing.py +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -17,9 +17,11 @@ from volatility3.plugins.linux import pslist vollog = logging.getLogger(__name__) -# https://github.com/SolitudePy/linux-mal class ProcessSpoofing(plugins.PluginInterface): - """Detects process spoofing by comparing executable path to cmdline & comm fields""" + """Detects process spoofing by comparing executable path to cmdline & comm fields. + + Examples of such behavior can be found here: https://github.com/SolitudePy/linux-mal + """ _required_framework_version = (2, 27, 0) _version = (1, 1, 0) @@ -101,7 +103,12 @@ class ProcessSpoofing(plugins.PluginInterface): task: interfaces.objects.ObjectInterface, ) -> Optional[str]: """ - Extract the command line arguments and return the basename of the first argument + Extract the command line arguments and return the basename of the first argument. + + Notes: + The read length is capped at ``MAX_ARG_STRLEN`` (32 * 4096) per the + kernel limit defined in ``include/uapi/linux/binfmts.h`` (see + linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940). Args: context: The context to operate on From b461e77c5e34dd82c8849ed5ef60d654c04ddba7 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 4 Jan 2026 14:14:42 +0000 Subject: [PATCH 105/128] Rebased version of the fix for issue1255 --- .../framework/automagic/symbol_cache.py | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index 327575e96..a347165f7 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -239,7 +239,16 @@ class SqliteCache(CacheManagerInterface): results = self._database.cursor().execute(statement, parameters).fetchall() result = None for row in results: - result = row["location"] + local_filepath = self._get_local_filepath(row["location"]) + if not ( + local_filepath is None + or local_filepath.startswith(tuple(constants.SYMBOL_BASEPATHS)) + ): + vollog.debug( + f"Location {row['location']} found but outside of the registered symbol paths" + ) + else: + result = row["location"] return result def get_local_locations(self) -> Generator[str, None, None]: @@ -249,7 +258,11 @@ class SqliteCache(CacheManagerInterface): .fetchall() ) for row in result: - yield row["location"] + local_filepath = self._get_local_filepath(row["location"]) + if local_filepath and local_filepath.startswith( + tuple(constants.SYMBOL_BASEPATHS) + ): + yield row["location"] def is_url_local(self, url: str) -> bool: """Determines whether an url is local or not""" @@ -296,6 +309,20 @@ class SqliteCache(CacheManagerInterface): return row["hash"] return None + def _get_local_filepath( + self, location: str, local_only: bool = True + ) -> Optional[str]: + # See if the file is a local URL type we can handle: + parsed = urllib.parse.urlparse(location) + pathname = location if not local_only else None + if parsed.scheme == "file": + pathname = parsed.path + if parsed.scheme == "jar": + inner_url = urllib.parse.urlparse(parsed.path) + if inner_url.scheme == "file": + pathname = inner_url.path.split("!")[0] + return pathname + def update(self, progress_callback=None): """Locates all files under the symbol directories. Updates the cache with additions, modifications and removals. This also updates remote locations based on a cache timeout. @@ -343,7 +370,7 @@ class SqliteCache(CacheManagerInterface): parsed = urllib.parse.urlparse(location) pathname = None if parsed.scheme == "file": - pathname = urllib.request.url2pathname(parsed.path) + pathname = parsed.path if parsed.scheme == "jar": inner_url = urllib.parse.urlparse(parsed.path) if inner_url.scheme == "file": @@ -461,7 +488,7 @@ class SqliteCache(CacheManagerInterface): def get_identifier_dictionary( self, operating_system: Optional[str] = None, local_only: bool = False ) -> Dict[bytes, str]: - output = {} + output: Dict[bytes, str] = {} additions = [] statement = "SELECT location, identifier FROM cache" if local_only: @@ -476,7 +503,15 @@ class SqliteCache(CacheManagerInterface): vollog.debug( f"Duplicate entry for identifier {row['identifier']}: {row['location']} and {output[row['identifier']]}" ) - output[row["identifier"]] = row["location"] + local_filepath = self._get_local_filepath(row["location"]) + if local_filepath and not local_filepath.startswith( + tuple(constants.SYMBOL_BASEPATHS) + ): + vollog.debug( + f"Location {row['location']} was not in the registered symbol paths and therefore not in the identifier dictionary" + ) + else: + output[row["identifier"]] = row["location"] return output def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]: @@ -533,7 +568,10 @@ class SymbolCacheMagic(interfaces.automagic.AutomagicInterface): def __call__(self, context, config_path, configurable, progress_callback=None): """Runs the automagic over the configurable.""" - self._cache.update(progress_callback) + try: + self._cache.update(progress_callback) + except Exception as excp: + vollog.debug(f"Excption during cache update: {excp}") @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 0da666fbbacde629abfcc90998edefcb92aaec9a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 4 Jan 2026 14:19:32 +0000 Subject: [PATCH 106/128] Reduce code reuse --- volatility3/framework/automagic/symbol_cache.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index a347165f7..ff75e86c6 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -367,15 +367,7 @@ class SqliteCache(CacheManagerInterface): timestamp = stored_timestamp # Default to requiring update # See if the file is a local URL type we can handle: - parsed = urllib.parse.urlparse(location) - pathname = None - if parsed.scheme == "file": - pathname = parsed.path - if parsed.scheme == "jar": - inner_url = urllib.parse.urlparse(parsed.path) - if inner_url.scheme == "file": - pathname = inner_url.path.split("!")[0] - + pathname = self._get_local_filepath(location) if pathname and os.path.exists(pathname): timestamp = datetime.datetime.fromtimestamp( os.stat(pathname).st_mtime From 9d67420d0ebe827e508884b02230f5a42103a021 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 8 Jan 2026 21:59:03 +0000 Subject: [PATCH 107/128] Improve windows intel detection for Windows 11 --- volatility3/framework/automagic/windows.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 7d56b01b3..af5d4809d 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -153,8 +153,7 @@ class DtbSelfRefPae(DtbSelfReferential): # Mask off the page bits of top level page map page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4 page_table = data[ - top_pae_page - - data_offset : top_pae_page + top_pae_page - data_offset : top_pae_page - data_offset + (4 * self.ptr_size) ] @@ -200,7 +199,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): ( "Detecting Self-referential pointer for recent windows", [DtbSelfRef64bit()], - [(0x150000, 0x150000), (0x650000, 0xA0000)], + [(0x150000, 0x150000), (0x550000, 0xA0000)], ), ( "Older windows fixed location self-referential pointers", @@ -305,9 +304,20 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): hits = sorted(list(hits), key=sort_by_tests) + vollog.debug(f"WindowsIntelStacker hits: {hits}") + for test, page_map_offset in hits: # Turn the page tables into integers and find the largest one page_table = base_layer.read(page_map_offset, 0x1000) + + # Modern windows can have a dummy page table with only about 2 entries, so sanity check + null_count = sum([1 if page_table[x] else 0 for x in page_table]) + if null_count > 0xFA0: + vollog.debug( + f"DTB {page_map_offset:x} contains less than 12 valid pointers, ignoring" + ) + continue + ptr_size = struct.calcsize(test.ptr_struct) max_pointer = get_max_pointer(page_table, test, ptr_size) From 1abd67746f456eea3db2edbf14d6a79398de35d8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 8 Jan 2026 22:04:18 +0000 Subject: [PATCH 108/128] Appease black --- volatility3/framework/automagic/windows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index af5d4809d..5ba531d1a 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -153,7 +153,8 @@ class DtbSelfRefPae(DtbSelfReferential): # Mask off the page bits of top level page map page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4 page_table = data[ - top_pae_page - data_offset : top_pae_page + top_pae_page + - data_offset : top_pae_page - data_offset + (4 * self.ptr_size) ] From d0ed42456cb24fbcf3f10ec3419618c168cdc575 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 8 Jan 2026 22:34:32 +0000 Subject: [PATCH 109/128] Increment the size of the amount scanned, not just the start location --- volatility3/framework/automagic/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 5ba531d1a..cd584b6a1 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -200,7 +200,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): ( "Detecting Self-referential pointer for recent windows", [DtbSelfRef64bit()], - [(0x150000, 0x150000), (0x550000, 0xA0000)], + [(0x150000, 0x150000), (0x550000, 0x1A0000)], ), ( "Older windows fixed location self-referential pointers", From 9949565bd8c0690154773b622a70f4dc7e917306 Mon Sep 17 00:00:00 2001 From: kyrre Date: Wed, 14 Jan 2026 19:55:43 +0100 Subject: [PATCH 110/128] use pa.bytes instead of pa.utf8 for format_hints.MultiTypeData --- volatility3/framework/plugins/renderers/parquet_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index f4eaf9c9b..c08fa7231 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -52,7 +52,7 @@ class ArrowRenderer(text_renderer.CLIRenderer): datetime.datetime: lambda: pa.timestamp("ms"), format_hints.Bin: pa.uint64, format_hints.Hex: pa.uint64, - format_hints.MultiTypeData: pa.utf8, + format_hints.MultiTypeData: pa.binary, format_hints.HexBytes: pa.binary, renderers.LayerData: pa.binary, bytes: pa.binary, From 881d4e85e8cd7864a613e70240e2ef1c1415752e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 31 Jan 2026 17:53:30 +0000 Subject: [PATCH 111/128] Add in intial support for locating windows 'banners' --- volatility3/framework/plugins/banners.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index eea39206d..85a6f4862 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -4,10 +4,11 @@ import logging from typing import List -from volatility3.framework import interfaces, renderers, layers +from volatility3.framework import constants, interfaces, layers, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows import pdbutil vollog = logging.getLogger(__name__) @@ -16,6 +17,7 @@ class Banners(interfaces.plugins.PluginInterface): """Attempts to identify potential linux banners in an image""" _required_framework_version = (2, 0, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -42,6 +44,7 @@ class Banners(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, layer_name: str ): """Identifies banners from a memory image""" + # Look for likely linux/mac banners layer = context.layers[layer_name] for offset in layer.scan( context=context, @@ -64,6 +67,25 @@ class Banners(interfaces.plugins.PluginInterface): format_hints.Hex(offset), str(data, encoding="latin-1", errors="?"), ) + yield from cls.locate_windows_banners(context, layer_name) + + @classmethod + def locate_windows_banners( + cls, context: interfaces.context.ContextInterface, layer_name: str + ): + layer = context.layers[layer_name] + kernel_pdb_names = [ + bytes(name + ".pdb", "utf-8") + for name in constants.windows.KERNEL_MODULE_NAMES + ] + for guid, age, pdb_name, offset in layer.scan( + context=context, + scanner=pdbutil.PdbSignatureScanner(kernel_pdb_names), + ): + yield ( + format_hints.Hex(offset), + f"{pdb_name.decode('latin-1')} - {guid}-{age}", + ) def run(self): return renderers.TreeGrid( From 1b58e7e25b319785a116c1d7bdbeafbda58a754e Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 31 Jan 2026 17:58:45 +0000 Subject: [PATCH 112/128] Add version to pdb signature scanner and require it for the banners plugin --- volatility3/framework/plugins/banners.py | 5 +++++ volatility3/framework/symbols/windows/pdbutil.py | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index 85a6f4862..2b354edcb 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -30,6 +30,11 @@ class Banners(interfaces.plugins.PluginInterface): component=scanners.RegExScanner, version=(1, 0, 0), ), + requirements.VersionRequirement( + name="pdb_signature_scanner", + component=pdbutil.PdbSignatureScanner, + version=(1, 0, 0), + ), ] def _generator(self): diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index 5f5c8cac8..d1ea4abd5 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -525,6 +525,10 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface): .. note:: The pdb_names must be a list of byte strings, unicode strs will not match against the data scanned """ + _version = (1, 0, 0) + + _required_framework_version = (2, 27, 0) + overlap = 0x4000 """The size of overlap needed for the signature to ensure data cannot hide between two scanned chunks""" thread_safe = True @@ -548,9 +552,7 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface): ) for match in re.finditer(pattern, data, flags=re.DOTALL): pdb_name = data[ - match.start(0) - + 4 - + self._RSDS_format.size : match.start(0) + match.start(0) + 4 + self._RSDS_format.size : match.start(0) + len(match.group()) - 1 ] From 743927d6a2c3ebb2c531e274127f91062ff2455a Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 31 Jan 2026 18:00:07 +0000 Subject: [PATCH 113/128] Make black happy --- volatility3/framework/symbols/windows/pdbutil.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index d1ea4abd5..af86f6997 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -552,7 +552,9 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface): ) for match in re.finditer(pattern, data, flags=re.DOTALL): pdb_name = data[ - match.start(0) + 4 + self._RSDS_format.size : match.start(0) + match.start(0) + + 4 + + self._RSDS_format.size : match.start(0) + len(match.group()) - 1 ] From 48a5000f78fcd386cf29673104a7ee917756aa16 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sat, 31 Jan 2026 18:03:55 +0000 Subject: [PATCH 114/128] Make the output of banners match the identifier in isfinfo --- volatility3/framework/plugins/banners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index 2b354edcb..69a1ee2b7 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -89,7 +89,7 @@ class Banners(interfaces.plugins.PluginInterface): ): yield ( format_hints.Hex(offset), - f"{pdb_name.decode('latin-1')} - {guid}-{age}", + f"{pdb_name.decode('latin-1')}|{guid}|{age}", ) def run(self): From fd22429b0b5a570d04dd5bdc7515df8e63f38c0d Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 19 Feb 2026 20:20:10 +0000 Subject: [PATCH 115/128] Ubunutu-latest no longer supports 3.9 --- .github/workflows/install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 9b9cbed4d..fbc07cae9 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.8", "3.9", "3.10", "3.11" ] + python-version: [ "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 From 81fae3a83b604aff08d7a376397117a572522711 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 19 Feb 2026 22:48:03 +0000 Subject: [PATCH 116/128] We weren't breaking between accumulating lines and outputting them --- volatility3/framework/plugins/timeliner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index f65868705..ddcc393f4 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -153,6 +153,8 @@ orders the results by time.""" ) times[timestamp_type] = timestamp self.timeline[(plugin_name, item)] = times + + for plugin_name, item in self.timeline: data.append( ( 0, From e095851ab962dc8ee8a257c92f26ea3b60b9e345 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Fri, 20 Feb 2026 18:34:33 +0000 Subject: [PATCH 117/128] Improve Win11 DTB detection It seems windows 11 has started moving the DTB further afield. This change adds another region located empirically, so it might need extending for everything between the second and third regions potentially. It may also be possible to shrink the third region, but that will need more example images (it's still not clear what causes the use of the higher DTB, although the image it was found in was 18Gb). --- volatility3/framework/automagic/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index cd584b6a1..bed2a4376 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -200,7 +200,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): ( "Detecting Self-referential pointer for recent windows", [DtbSelfRef64bit()], - [(0x150000, 0x150000), (0x550000, 0x1A0000)], + [(0x150000, 0x150000), (0x550000, 0x1A0000), (0x900000, 0x100000)], ), ( "Older windows fixed location self-referential pointers", From 5606baf4e604d91b62813adedd23d1eb617a98b7 Mon Sep 17 00:00:00 2001 From: "androsh7@gmail.com" Date: Sun, 1 Mar 2026 22:14:40 -0500 Subject: [PATCH 118/128] Added argument to set default encoding to utf-8 --- vol.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vol.py b/vol.py index c49d5985d..8c9701bfd 100755 --- a/vol.py +++ b/vol.py @@ -5,7 +5,12 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import sys + import volatility3.cli if __name__ == "__main__": + # Ensure stdout/stderr use UTF-8 to avoid output encoding errors on Windows systems + sys.stderr.reconfigure(encoding="utf-8") + sys.stdout.reconfigure(encoding="utf-8") volatility3.cli.main() From 55cd4d9759931036e94d781795ec99e46049423c Mon Sep 17 00:00:00 2001 From: Abyss Watcher Date: Fri, 6 Mar 2026 14:24:48 +0100 Subject: [PATCH 119/128] adjust dummy page table check --- volatility3/framework/automagic/windows.py | 38 +++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index bed2a4376..f627043db 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -287,22 +287,38 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): """Key used to sort by tests""" return tests.index(x[0]), x[1] - def get_max_pointer(page_table, test, ptr_size: int): - """Determines a pointer from a page_table""" - max_ptr = 0 + def get_valid_page_table_pointers(page_table, ptr_size: int): + """Yields valid pointers from a page table""" for index in range(0, len(page_table), ptr_size): pointer = struct.unpack( test.ptr_struct, page_table[index : index + ptr_size] )[0] # Make sure the pointer is valid, ignore large pages which would require more calculation if pointer & 0x1 and not pointer & 0x80: - max_ptr = max( - max_ptr, - (pointer ^ (pointer & 0xFFF)) - % test.layer_type.maximum_address, - ) + yield pointer + + def get_max_pointer(page_table, test, ptr_size: int): + """Determines a pointer from a page_table""" + max_ptr = 0 + for pointer in get_valid_page_table_pointers(page_table, ptr_size): + max_ptr = max( + max_ptr, + (pointer ^ (pointer & 0xFFF)) % test.layer_type.maximum_address, + ) return max_ptr + def page_table_is_dummy(page_table, ptr_size: int): + """Verify that a page table has at least 12 valid pointers""" + valid_pointers = 0 + for _ in get_valid_page_table_pointers(page_table, ptr_size): + valid_pointers += 1 + # 12 is an arbitrary constant + if valid_pointers >= 12: + # Do not consume the entire generator to enhance performance + return False + + return True + hits = sorted(list(hits), key=sort_by_tests) vollog.debug(f"WindowsIntelStacker hits: {hits}") @@ -310,16 +326,14 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): for test, page_map_offset in hits: # Turn the page tables into integers and find the largest one page_table = base_layer.read(page_map_offset, 0x1000) - + ptr_size = struct.calcsize(test.ptr_struct) # Modern windows can have a dummy page table with only about 2 entries, so sanity check - null_count = sum([1 if page_table[x] else 0 for x in page_table]) - if null_count > 0xFA0: + if page_table_is_dummy(page_table, ptr_size): vollog.debug( f"DTB {page_map_offset:x} contains less than 12 valid pointers, ignoring" ) continue - ptr_size = struct.calcsize(test.ptr_struct) max_pointer = get_max_pointer(page_table, test, ptr_size) if max_pointer <= base_layer.maximum_address: From d3a9481a31d092963dc3c9e50b5da435d3f6ab14 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Mar 2026 20:29:18 +0000 Subject: [PATCH 120/128] Tidy the threshold code for empty DTBs using code from @abyss-w4tcher and reduce it slightly --- volatility3/framework/automagic/windows.py | 37 ++++++++++++++-------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index bed2a4376..b9c9bedf9 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -153,8 +153,7 @@ class DtbSelfRefPae(DtbSelfReferential): # Mask off the page bits of top level page map page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4 page_table = data[ - top_pae_page - - data_offset : top_pae_page + top_pae_page - data_offset : top_pae_page - data_offset + (4 * self.ptr_size) ] @@ -287,22 +286,35 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): """Key used to sort by tests""" return tests.index(x[0]), x[1] - def get_max_pointer(page_table, test, ptr_size: int): - """Determines a pointer from a page_table""" - max_ptr = 0 + def get_valid_page_table_pointers(page_table, ptr_size: int): for index in range(0, len(page_table), ptr_size): pointer = struct.unpack( test.ptr_struct, page_table[index : index + ptr_size] )[0] # Make sure the pointer is valid, ignore large pages which would require more calculation if pointer & 0x1 and not pointer & 0x80: - max_ptr = max( - max_ptr, - (pointer ^ (pointer & 0xFFF)) - % test.layer_type.maximum_address, - ) + yield pointer + + def get_max_pointer(page_table, test, ptr_size: int): + """Determines a pointer from a page_table""" + max_ptr = 0 + for pointer in get_valid_page_table_pointers(page_table, ptr_size): + max_ptr = max( + max_ptr, + (pointer ^ (pointer & 0xFFF)) % test.layer_type.maximum_address, + ) return max_ptr + def page_table_is_dummy(page_table, ptr_size): + valid_pointers = 0 + for _ in get_valid_page_table_pointers(page_table, ptr_size): + valid_pointers += 1 + if valid_pointers >= 10: + # Do not consume the entire generator to enhance performance + return False + vollog.debug(f"Found {valid_pointers} valid pointers") + return True + hits = sorted(list(hits), key=sort_by_tests) vollog.debug(f"WindowsIntelStacker hits: {hits}") @@ -310,16 +322,15 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): for test, page_map_offset in hits: # Turn the page tables into integers and find the largest one page_table = base_layer.read(page_map_offset, 0x1000) + ptr_size = struct.calcsize(test.ptr_struct) # Modern windows can have a dummy page table with only about 2 entries, so sanity check - null_count = sum([1 if page_table[x] else 0 for x in page_table]) - if null_count > 0xFA0: + if page_table_is_dummy(page_table, ptr_size): vollog.debug( f"DTB {page_map_offset:x} contains less than 12 valid pointers, ignoring" ) continue - ptr_size = struct.calcsize(test.ptr_struct) max_pointer = get_max_pointer(page_table, test, ptr_size) if max_pointer <= base_layer.maximum_address: From 98e2f7a1f5132064497009451e443c9ced8d03b8 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Mar 2026 20:35:33 +0000 Subject: [PATCH 121/128] Put back in the debugging line --- volatility3/framework/automagic/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index af4fa4ca9..e57bf6974 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -315,7 +315,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): if valid_pointers >= 10: # Do not consume the entire generator to enhance performance return False - + vollog.debug(f"Found {valid_pointers} valid pointers") return True hits = sorted(list(hits), key=sort_by_tests) From 013921e78728a464d5ddf4b545bbbd188e257fdb Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 8 Mar 2026 20:48:02 +0000 Subject: [PATCH 122/128] Switch to using ruff for formatting as well as linting --- .github/workflows/black.yml | 15 ------------- .github/workflows/ruff.yaml | 4 +++- volatility3/cli/text_renderer.py | 6 ++--- volatility3/framework/automagic/linux.py | 4 +--- volatility3/framework/layers/avml.py | 3 +-- volatility3/framework/objects/__init__.py | 6 ++--- .../plugins/linux/malware/malfind.py | 7 +++++- .../framework/plugins/linux/pidhashtable.py | 8 ++----- volatility3/framework/plugins/linux/proc.py | 6 ++--- .../plugins/linux/tracing/perf_events.py | 4 +++- .../framework/plugins/mac/proc_maps.py | 6 ++--- volatility3/framework/plugins/mac/pslist.py | 4 +++- .../plugins/windows/getservicesids.py | 4 +--- .../plugins/windows/malware/pebmasquerade.py | 4 +++- .../framework/plugins/windows/pslist.py | 22 ++++++++++--------- .../framework/plugins/windows/psscan.py | 6 ++--- .../framework/plugins/windows/pstree.py | 12 +++++----- .../framework/plugins/windows/thrdscan.py | 8 ++----- .../framework/plugins/windows/vadinfo.py | 10 +++++---- .../symbols/windows/extensions/pool.py | 6 ++--- .../framework/symbols/windows/pdbutil.py | 4 +--- 21 files changed, 67 insertions(+), 82 deletions(-) delete mode 100644 .github/workflows/black.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 3df690543..000000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Black python formatter - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: psf/black@stable - with: - options: "--check --diff --verbose" - src: "./volatility3" - # FIXME: Remove when Volatility3 minimum Python version is >3.8 - version: "24.8.0" diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index 98a05a616..e2381dab2 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -4,7 +4,7 @@ name: Ruff on: [push, pull_request] jobs: - lint: + lint-and-format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -13,3 +13,5 @@ jobs: with: args: check src: "." + + - run: "ruff format --check --diff" diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d00c00bf4..d400067af 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( - [] - ) + final_output: List[ + Tuple[int, Dict[interfaces.renderers.Column, list[str]]] + ] = [] if not grid.populated: grid.populate(visitor, final_output) else: diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index 95703aaf7..511b95731 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -165,9 +165,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): "init_mm" ).address and init_task.tasks.next.cast( "long unsigned int" - ) == init_task.tasks.prev.cast( - "long unsigned int" - ): + ) == init_task.tasks.prev.cast("long unsigned int"): # The idle task steals `mm` from previously running task, i.e., # `init_mm` is only used as long as no CPU has ever been idle. # This catches cases where we found a fragment of the diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index 7c052f70a..841d821cb 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -160,8 +160,7 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer): if frame_type == 0xFF: if ( data[ - offset - + frame_header_len : offset + offset + frame_header_len : offset + frame_header_len + frame_size ] diff --git a/volatility3/framework/objects/__init__.py b/volatility3/framework/objects/__init__.py index 08d6cb31e..40eb532f1 100644 --- a/volatility3/framework/objects/__init__.py +++ b/volatility3/framework/objects/__init__.py @@ -948,9 +948,9 @@ class AggregateType(interfaces.objects.ObjectInterface): if isinstance(cls, agg_type): agg_name = agg_type.__name__ - assert isinstance( - members, collections.abc.Mapping - ), f"{agg_name} members parameter must be a mapping: {type(members)}" + assert isinstance(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() diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index cbd9f87c1..533a4e217 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -88,7 +88,12 @@ class Malfind(interfaces.plugins.PluginInterface): for page_addr in malicious_pages: offset = page_addr - vma.vm_start data = proc_layer.read(page_addr, dump_size, pad=True) - yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset + yield ( + vma, + f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", + data, + offset, + ) else: # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) data = proc_layer.read(vma.vm_start, dump_size, pad=True) diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index b4b1643e1..0324ed846 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -194,15 +194,11 @@ class PIDHashTable(plugins.PluginInterface): has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type( "pid" - ).has_member( - "numbers" - ) # kernels >= 2.6.24 + ).has_member("numbers") # kernels >= 2.6.24 has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type( "upid" - ).has_member( - "pid_chain" - ) # 2.6.24 <= kernels < 4.15 + ).has_member("pid_chain") # 2.6.24 <= kernels < 4.15 # kernels >= 4.15 pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type( diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index e9a126374..2c6ebd825 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -70,9 +70,9 @@ class Maps(plugins.PluginInterface): def list_vmas( cls, task: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: True, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + True + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Memory Areas of a specific process. diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index ff922784d..c7e4dbe34 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -34,7 +34,9 @@ class PerfEvents(plugins.PluginInterface): ] @classmethod - def list_perf_events(cls, context, vmlinux_module_name: str) -> Generator[ + def list_perf_events( + cls, context, vmlinux_module_name: str + ) -> Generator[ Tuple[ interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface, diff --git a/volatility3/framework/plugins/mac/proc_maps.py b/volatility3/framework/plugins/mac/proc_maps.py index 87f3559ea..b1370afdb 100644 --- a/volatility3/framework/plugins/mac/proc_maps.py +++ b/volatility3/framework/plugins/mac/proc_maps.py @@ -65,9 +65,9 @@ class Maps(interfaces.plugins.PluginInterface): def list_vmas( cls, task: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: True, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + True + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Memory Areas of a specific process. diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 904e4e201..f36bcd831 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -49,7 +49,9 @@ class PsList(interfaces.plugins.PluginInterface): ] @classmethod - def get_list_tasks(cls, method: str) -> Callable[ + def get_list_tasks( + cls, method: str + ) -> Callable[ [interfaces.context.ContextInterface, str, Callable[[int], bool]], Iterable[interfaces.objects.ObjectInterface], ]: diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index c04472eab..96786b586 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -19,9 +19,7 @@ vollog = logging.getLogger(__name__) def createservicesid(svc) -> str: """Calculate the Service SID""" uni = "".join([c + "\x00" for c in svc]) - sha = hashlib.sha1( - uni.upper().encode("utf-8") - ).digest() # pylint: disable-msg=E1101 + sha = hashlib.sha1(uni.upper().encode("utf-8")).digest() # pylint: disable-msg=E1101 dec = list() for i in range(5): ## The use of struct here is OK. It doesn't make much sense diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index cd898239f..867609f16 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -37,7 +37,9 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ] @classmethod - def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[ + def get_process_names( + cls, proc: interfaces.objects.ObjectInterface + ) -> Tuple[ Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 1043f8b42..db3e5dc99 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -167,13 +167,15 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Filter function for passing to the `list_processes` method """ - return lambda x: not ( - x.is_valid() - and x.ActiveThreads > 0 - and x.UniqueProcessId != 4 - and x.InheritedFromUniqueProcessId != 4 - and x.ExitTime.QuadPart == 0 - and x.get_handle_count() != renderers.UnreadableValue() + return lambda x: ( + not ( + x.is_valid() + and x.ActiveThreads > 0 + and x.UniqueProcessId != 4 + and x.InheritedFromUniqueProcessId != 4 + and x.ExitTime.QuadPart == 0 + and x.get_handle_count() != renderers.UnreadableValue() + ) ) @classmethod @@ -214,9 +216,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Iterator["extensions.EPROCESS"]: """Lists all the processes in the given layer that are in the pid config option. diff --git a/volatility3/framework/plugins/windows/psscan.py b/volatility3/framework/plugins/windows/psscan.py index ae37c20a1..de69c23be 100644 --- a/volatility3/framework/plugins/windows/psscan.py +++ b/volatility3/framework/plugins/windows/psscan.py @@ -150,9 +150,9 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for processes using the poolscanner module and constraints. diff --git a/volatility3/framework/plugins/windows/pstree.py b/volatility3/framework/plugins/windows/pstree.py index 373c555f6..c8d9a3f66 100644 --- a/volatility3/framework/plugins/windows/pstree.py +++ b/volatility3/framework/plugins/windows/pstree.py @@ -53,9 +53,9 @@ class PsTree(interfaces.plugins.PluginInterface): def find_level( self, pid: int, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> None: """Finds how deep the pid is in the processes list.""" seen = {pid} @@ -77,9 +77,9 @@ class PsTree(interfaces.plugins.PluginInterface): def _generator( self, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ): """Generates the Tree of processes.""" kernel = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 1588f292e..4b2bf47d2 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -99,12 +99,8 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) thread_tid = ethread.Cid.UniqueThread thread_start_addr = ethread.StartAddress thread_win32start_addr = ethread.Win32StartAddress - thread_create_time = ( - ethread.get_create_time() - ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object - thread_exit_time = ( - ethread.get_exit_time() - ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object + thread_create_time = ethread.get_create_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object + thread_exit_time = ethread.get_exit_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object owner_proc = None if vads_cache is not None: diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 22d42505f..25abd729e 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -115,9 +115,9 @@ class VadInfo(interfaces.plugins.PluginInterface): def list_vads( cls, proc: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Address Descriptors of a specific process. @@ -198,7 +198,9 @@ class VadInfo(interfaces.plugins.PluginInterface): return file_handle - def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[ + def _generator( + self, procs: List[interfaces.objects.ObjectInterface] + ) -> Generator[ Tuple[ int, Tuple[ diff --git a/volatility3/framework/symbols/windows/extensions/pool.py b/volatility3/framework/symbols/windows/extensions/pool.py index f12182fa7..a3bc3aefe 100644 --- a/volatility3/framework/symbols/windows/extensions/pool.py +++ b/volatility3/framework/symbols/windows/extensions/pool.py @@ -126,8 +126,7 @@ class POOL_HEADER(objects.StructType): infomask_value = infomask_data[addr + infomask_offset] pointercount_value = int.from_bytes( infomask_data[ - addr - + pointercount_offset : addr + addr + pointercount_offset : addr + pointercount_offset + pointercount_size ], @@ -165,8 +164,7 @@ class POOL_HEADER(objects.StructType): (padding_length,) = struct.unpack( " Date: Mon, 9 Mar 2026 20:47:24 +0000 Subject: [PATCH 123/128] Update documentation to remove black and use ruff --- CODING_STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index a4e248ffe..ed69003c6 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -5,7 +5,7 @@ The coding standards for volatility are mostly by our linter and our code format All code submissions will be vetted automatically through tests from both and the submission will not be accepted if either of these fail. Code Linter: Ruff -Code Formatter: Black +Code Formatter: Ruff In addition, there are some coding practices that we employ to prevent specific failure cases and ensure consistency across the codebase. These are documented below along with the rationale for the decision. From 5e955e522004ca1aa20702ae93b41e6468ef0af2 Mon Sep 17 00:00:00 2001 From: Esa Jokinen Date: Tue, 10 Mar 2026 17:08:01 +0200 Subject: [PATCH 124/128] Support Cryptodome namespace when Crypto is unavailable --- .../framework/plugins/windows/registry/cachedump.py | 9 +++++++-- .../framework/plugins/windows/registry/hashdump.py | 6 +++++- .../framework/plugins/windows/registry/lsadump.py | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/registry/cachedump.py b/volatility3/framework/plugins/windows/registry/cachedump.py index 49c5495c2..5e138ba99 100644 --- a/volatility3/framework/plugins/windows/registry/cachedump.py +++ b/volatility3/framework/plugins/windows/registry/cachedump.py @@ -5,8 +5,13 @@ import logging from struct import unpack from typing import Tuple -from Crypto.Cipher import ARC4, AES -from Crypto.Hash import HMAC +try: + from Crypto.Cipher import ARC4, AES + from Crypto.Hash import HMAC +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, AES + from Cryptodome.Hash import HMAC from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 19bd60e81..6f8a2bce8 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -7,7 +7,11 @@ import logging from struct import pack, unpack from typing import List, Optional, Tuple -from Crypto.Cipher import AES, ARC4, DES +try: + from Crypto.Cipher import ARC4, DES, AES +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions, constants from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/registry/lsadump.py b/volatility3/framework/plugins/windows/registry/lsadump.py index 50ecaebc1..7233f82ac 100644 --- a/volatility3/framework/plugins/windows/registry/lsadump.py +++ b/volatility3/framework/plugins/windows/registry/lsadump.py @@ -6,7 +6,11 @@ from struct import unpack from typing import Optional import hashlib -from Crypto.Cipher import ARC4, DES, AES +try: + from Crypto.Cipher import ARC4, DES, AES +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements From 0b6229871fa8f6e854d27607f2b354a0371a6194 Mon Sep 17 00:00:00 2001 From: ikelos Date: Tue, 10 Mar 2026 21:09:05 +0000 Subject: [PATCH 125/128] Revert "Only act on local cache symbols under the symbol basepaths" --- .../framework/automagic/symbol_cache.py | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index ff75e86c6..327575e96 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -239,16 +239,7 @@ class SqliteCache(CacheManagerInterface): results = self._database.cursor().execute(statement, parameters).fetchall() result = None for row in results: - local_filepath = self._get_local_filepath(row["location"]) - if not ( - local_filepath is None - or local_filepath.startswith(tuple(constants.SYMBOL_BASEPATHS)) - ): - vollog.debug( - f"Location {row['location']} found but outside of the registered symbol paths" - ) - else: - result = row["location"] + result = row["location"] return result def get_local_locations(self) -> Generator[str, None, None]: @@ -258,11 +249,7 @@ class SqliteCache(CacheManagerInterface): .fetchall() ) for row in result: - local_filepath = self._get_local_filepath(row["location"]) - if local_filepath and local_filepath.startswith( - tuple(constants.SYMBOL_BASEPATHS) - ): - yield row["location"] + yield row["location"] def is_url_local(self, url: str) -> bool: """Determines whether an url is local or not""" @@ -309,20 +296,6 @@ class SqliteCache(CacheManagerInterface): return row["hash"] return None - def _get_local_filepath( - self, location: str, local_only: bool = True - ) -> Optional[str]: - # See if the file is a local URL type we can handle: - parsed = urllib.parse.urlparse(location) - pathname = location if not local_only else None - if parsed.scheme == "file": - pathname = parsed.path - if parsed.scheme == "jar": - inner_url = urllib.parse.urlparse(parsed.path) - if inner_url.scheme == "file": - pathname = inner_url.path.split("!")[0] - return pathname - def update(self, progress_callback=None): """Locates all files under the symbol directories. Updates the cache with additions, modifications and removals. This also updates remote locations based on a cache timeout. @@ -367,7 +340,15 @@ class SqliteCache(CacheManagerInterface): timestamp = stored_timestamp # Default to requiring update # See if the file is a local URL type we can handle: - pathname = self._get_local_filepath(location) + parsed = urllib.parse.urlparse(location) + pathname = None + if parsed.scheme == "file": + pathname = urllib.request.url2pathname(parsed.path) + if parsed.scheme == "jar": + inner_url = urllib.parse.urlparse(parsed.path) + if inner_url.scheme == "file": + pathname = inner_url.path.split("!")[0] + if pathname and os.path.exists(pathname): timestamp = datetime.datetime.fromtimestamp( os.stat(pathname).st_mtime @@ -480,7 +461,7 @@ class SqliteCache(CacheManagerInterface): def get_identifier_dictionary( self, operating_system: Optional[str] = None, local_only: bool = False ) -> Dict[bytes, str]: - output: Dict[bytes, str] = {} + output = {} additions = [] statement = "SELECT location, identifier FROM cache" if local_only: @@ -495,15 +476,7 @@ class SqliteCache(CacheManagerInterface): vollog.debug( f"Duplicate entry for identifier {row['identifier']}: {row['location']} and {output[row['identifier']]}" ) - local_filepath = self._get_local_filepath(row["location"]) - if local_filepath and not local_filepath.startswith( - tuple(constants.SYMBOL_BASEPATHS) - ): - vollog.debug( - f"Location {row['location']} was not in the registered symbol paths and therefore not in the identifier dictionary" - ) - else: - output[row["identifier"]] = row["location"] + output[row["identifier"]] = row["location"] return output def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]: @@ -560,10 +533,7 @@ class SymbolCacheMagic(interfaces.automagic.AutomagicInterface): def __call__(self, context, config_path, configurable, progress_callback=None): """Runs the automagic over the configurable.""" - try: - self._cache.update(progress_callback) - except Exception as excp: - vollog.debug(f"Excption during cache update: {excp}") + self._cache.update(progress_callback) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From 34384ddb4b167ef2ff0fe39794f973beeff3b0a2 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Thu, 12 Mar 2026 22:25:46 +0000 Subject: [PATCH 126/128] Switch to using github infrastructure for test cases... --- .github/workflows/test.yaml | 10 +++++----- README.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 266e8bcc3..104b1f9d8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,19 +26,19 @@ jobs: run: | mkdir test_images cd test_images - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux-sample-1.bin.gz" gunzip linux-sample-1.bin.gz - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-xp-laptop-2005-06-25.img.gz" gunzip win-xp-laptop-2005-06-25.img.gz - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-10_19041-2025_03.dmp.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-10_19041-2025_03.dmp.gz" gunzip win-10_19041-2025_03.dmp.gz cd - - name: Download and Extract symbols run: | cd ./volatility3/symbols - curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip - curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/symbols_win-10_19041-2025_03.zip + curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip + curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/symbols_win-10_19041-2025_03.zip unzip linux.zip unzip symbols_win-10_19041-2025_03.zip cd - diff --git a/README.md b/README.md index 8c5332a90..e38975f91 100644 --- a/README.md +++ b/README.md @@ -65,19 +65,19 @@ pip install -e ".[dev]" Symbol table packs for the various operating systems are available for download at: - +[windows.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/windows.zip) - +[mac.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/mac.zip) - +[linux.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip) The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: - +[SHA256SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA256SUMS) - +[SHA1SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA1SUMS) - +[MD5SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/MD5SUMS) Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file). From dd06faa7ce3902fc327eb5a8ac48592e3621f06e Mon Sep 17 00:00:00 2001 From: Esa Jokinen Date: Mon, 16 Mar 2026 08:47:19 +0200 Subject: [PATCH 127/128] Add user cache directory as fallback for downloaded symbols --- volatility3/framework/constants/__init__.py | 27 ++++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 689ef122b..684ba7cfd 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -25,6 +25,18 @@ from volatility3.framework.constants._version import ( REQUIRED_PYTHON_VERSION = (3, 8, 0) +CACHE_PATH = os.path.join( + os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"), + "volatility3", +) +"""Default path to store cached data""" + +if sys.platform == "win32": + CACHE_PATH = os.path.realpath( + os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3") + ) +os.makedirs(CACHE_PATH, exist_ok=True) + PLUGINS_PATH = [ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")), os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")), @@ -34,6 +46,9 @@ PLUGINS_PATH = [ SYMBOL_BASEPATHS = [ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "symbols")), os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "symbols")), + os.path.abspath( + os.path.join(CACHE_PATH, "symbols") + ), # User cache fallback for automatically downloaded temporary symbols ] """Default list of paths to load symbols from (volatility3/symbols and volatility3/framework/symbols)""" @@ -71,21 +86,9 @@ LOGLEVEL_VVVV = 6 """Logging level for four levels of detail: -vvvvvv""" -CACHE_PATH = os.path.join( - os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"), - "volatility3", -) -"""Default path to store cached data""" - SQLITE_CACHE_PERIOD = "-3 days" """SQLite time modifier for how long each item is valid in the cache for""" -if sys.platform == "win32": - CACHE_PATH = os.path.realpath( - os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3") - ) -os.makedirs(CACHE_PATH, exist_ok=True) - IDENTIFIERS_FILENAME = "identifier.cache" """Default location to record information about available identifiers""" From 7707a140926595af9e07012b4476e8bdb03de4e1 Mon Sep 17 00:00:00 2001 From: Esa Jokinen Date: Mon, 16 Mar 2026 15:44:07 +0200 Subject: [PATCH 128/128] PDBUtility: Log per-path symbol write failures at DEBUG level --- volatility3/framework/symbols/windows/pdbutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index af86f6997..ccee64c55 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -289,7 +289,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface): ) break except PermissionError: - vollog.warning( + vollog.debug( f"Cannot write necessary symbol file, please check permissions on {potential_output_filename}" ) continue