diff --git a/.gitignore b/.gitignore index 328ba5f83..7c610b637 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ workspace.xml # Manually generated files .mypy_cache stubs +volatility3/symbols/freebsd* volatility3/symbols/linux* volatility3/symbols/windows* volatility3/symbols/mac* diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 035ed9b2e..f543158b1 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -11,7 +11,7 @@ import volatility3.plugins import volatility3.symbols from volatility3 import cli, framework -from volatility3.cli.volshell import generic, linux, mac, windows +from volatility3.cli.volshell import freebsd, generic, linux, mac, windows from volatility3.framework import ( automagic, constants, @@ -167,6 +167,7 @@ def run(self): action="store_true", help="Run a Windows volshell", ) + os_specific.add_argument("--freebsd", default = False, action = "store_true", help = "Run a Freebsd volshell") os_specific.add_argument( "-l", "--linux", @@ -285,6 +286,8 @@ def run(self): plugin = generic.Volshell if args.windows: plugin = windows.Volshell + if args.freebsd: + plugin = freebsd.Volshell if args.linux: plugin = linux.Volshell if args.mac: diff --git a/volatility3/cli/volshell/freebsd.py b/volatility3/cli/volshell/freebsd.py new file mode 100644 index 000000000..574a86b6e --- /dev/null +++ b/volatility3/cli/volshell/freebsd.py @@ -0,0 +1,74 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import Any, List, Tuple, Union + +from volatility3.cli.volshell import generic +from volatility3.framework import constants, interfaces +from volatility3.framework.configuration import requirements +from volatility3.plugins.freebsd import pslist + + +class Volshell(generic.Volshell): + """Shell environment to directly interact with a freebsd memory image.""" + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement(name = "kernel", description = "Kernel module for the OS"), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.IntRequirement(name = "pid", description = "Process ID", optional = True), + ] + + def change_task(self, pid = None): + """Change the current process and layer, based on a process ID""" + tasks = self.list_tasks() + for task in tasks: + if task.p_pid == pid: + process_layer = task.add_process_layer() + if process_layer is not None: + self.change_layer(process_layer) + return None + print(f"Layer for task ID {pid} could not be constructed") + return None + print(f"No task with task ID {pid} found") + + def list_tasks(self): + """Returns a list of task objects from the primary layer""" + # We always use the main kernel memory and associated symbols + return list(pslist.PsList.list_tasks(self.context, self.current_kernel_name)) + + def construct_locals(self) -> List[Tuple[List[str], Any]]: + result = super().construct_locals() + result += [ + (["ct", "change_task", "cp"], self.change_task), + (["lt", "list_tasks", "ps"], self.list_tasks), + (["symbols"], self.context.symbol_space[self.current_symbol_table]), + ] + if self.config.get("pid", None) is not None: + self.change_task(self.config["pid"]) + return result + + def display_type( + self, + object: Union[str, interfaces.objects.ObjectInterface, interfaces.objects.Template], + offset: int = None, + ): + """Display Type describes the members of a particular object in alphabetical order""" + if isinstance(object, str): + if constants.BANG not in object: + object = self.current_symbol_table + constants.BANG + object + return super().display_type(object, offset) + + def display_symbols(self, symbol_table: str = None): + """Prints an alphabetical list of symbols for a symbol table""" + if symbol_table is None: + symbol_table = self.current_symbol_table + return super().display_symbols(symbol_table) + + @property + def current_layer(self): + if self.__current_layer is None: + self.__current_layer = self.kernel.layer_name + return self.__current_layer diff --git a/volatility3/framework/automagic/freebsd.py b/volatility3/framework/automagic/freebsd.py new file mode 100644 index 000000000..50884079d --- /dev/null +++ b/volatility3/framework/automagic/freebsd.py @@ -0,0 +1,151 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +import os +import struct +from typing import Optional, Type + +from volatility3.framework import constants, interfaces +from volatility3.framework.automagic import symbol_cache, symbol_finder +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import intel, scanners +from volatility3.framework.symbols import freebsd + +vollog = logging.getLogger(__name__) + + +class FreebsdIntelStacker(interfaces.automagic.StackerLayerInterface): + stack_order = 35 + exclusion_list = ["linux", "mac", "windows"] + + @classmethod + def stack( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + progress_callback: constants.ProgressCallback = None, + ) -> Optional[interfaces.layers.DataLayerInterface]: + """Attempts to identify freebsd within this layer.""" + # Version check the SQlite cache + required = (1, 0, 0) + if not requirements.VersionRequirement.matches_required(required, symbol_cache.SqliteCache.version): + vollog.info( + f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}") + return None + + # Bail out by default unless we can stack properly + layer = context.layers[layer_name] + join = interfaces.configuration.path_join + + # Never stack on top of an intel layer + # FIXME: Find a way to improve this check + if isinstance(layer, intel.Intel): + return None + + identifiers_path = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME) + freebsd_banners = symbol_cache.SqliteCache(identifiers_path).get_identifier_dictionary( + operating_system = "freebsd") + # If we have no banners, don't bother scanning + if not freebsd_banners: + vollog.info( + "No Freebsd banners found - if this is a freebsd plugin, please check your symbol files location") + return None + + mss = scanners.MultiStringScanner([x for x in freebsd_banners if x is not None]) + for _, banner in layer.scan(context = context, scanner = mss, progress_callback = progress_callback): + dtb = None + vollog.debug(f"Identified banner: {repr(banner)}") + + isf_path = freebsd_banners.get(banner, None) + if isf_path: + table_name = context.symbol_space.free_table_name("FreebsdIntelStacker") + table = freebsd.FreebsdKernelIntermedSymbols( + context, + "temporary." + table_name, + name = table_name, + isf_url = isf_path, + ) + context.symbol_space.append(table) + + layer_class: Type = intel.Intel + # Freebsd amd64 + if "KPML4phys" in table.symbols: + layer_class = intel.Intel32e + kernload_offset = 0 + kernload = table.get_symbol("kernload").address + for interp in layer.scan(context = context, scanner = scanners.BytesScanner(b"/red/herring\x00\x00\x00\x00"), progress_callback = progress_callback): + kernload_from_interp = interp & 0xfffffffffffff800 + # Verify 2MB alignment + if kernload_from_interp & 0x1fffff == 0: + kernload_offset = kernload_from_interp - kernload + break + kernbase = table.get_symbol("kernbase").address + kpml4phys_ptr = table.get_symbol("KPML4phys").address + kpml4phys_str = layer.read(kpml4phys_ptr - kernbase + kernload_offset, 8) + dtb = struct.unpack(" Optional[bytes]: mac_banner = ( json.get("symbols", {}).get("version", {}).get("constant_data", None) ) + if mac_banner and json.get("symbols", {}).get("kfreebsd_brand_info", {}): + return None if mac_banner: return base64.b64decode(mac_banner) return None @@ -93,6 +95,19 @@ def get_identifier(cls, json) -> Optional[bytes]: return None +class FreebsdIdentifier(IdentifierProcessor): + operating_system = "freebsd" + + @classmethod + def get_identifier(cls, json) -> Optional[bytes]: + freebsd_banner = (json.get("symbols", {}).get("version", {}).get("constant_data", None)) + if freebsd_banner and not json.get("symbols", {}).get("kfreebsd_brand_info", {}): + return None + if freebsd_banner: + return base64.b64decode(freebsd_banner) + return None + + ### CacheManagers diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 52296f5ad..a8a2bfbc1 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -192,7 +192,7 @@ def __call__( class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): stack_order = 40 - exclusion_list = ["mac", "linux"] + exclusion_list = ["freebsd", "linux", "mac"] # Group these by region so we only run over the data once test_sets = [ @@ -357,7 +357,7 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface): """Class to read swap_layers filenames from single-swap-layers, create the layers and populate the single-layers swap_layers.""" - exclusion_list = ["linux", "mac"] + exclusion_list = ["freebsd", "linux", "mac"] def __call__( self, diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 6963867f7..42078c4be 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -95,7 +95,7 @@ ProgressCallback = Optional[Callable[[float, str], None]] """Type information for ProgressCallback objects""" -OS_CATEGORIES = ["windows", "mac", "linux"] +OS_CATEGORIES = ["windows", "mac", "linux", "freebsd"] class Parallelism(enum.IntEnum): diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index b3c2fd3a5..605563932 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -41,7 +41,7 @@ def locate_banners( for offset in layer.scan( context=context, scanner=scanners.RegExScanner( - rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+" + rb"((Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+|FreeBSD [0-9]+\.[0-9]+)" ), ): data = layer.read(offset, 0xFFF) diff --git a/volatility3/framework/plugins/freebsd/__init__.py b/volatility3/framework/plugins/freebsd/__init__.py new file mode 100644 index 000000000..add05c9f1 --- /dev/null +++ b/volatility3/framework/plugins/freebsd/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core freebsd plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/freebsd/creds.py b/volatility3/framework/plugins/freebsd/creds.py new file mode 100644 index 000000000..38aeea45c --- /dev/null +++ b/volatility3/framework/plugins/freebsd/creds.py @@ -0,0 +1,62 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import Iterator, Tuple, Any, Generator, List + +from volatility3.framework import constants, exceptions, renderers, interfaces, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.freebsd import pslist + + +class Creds(plugins.PluginInterface): + """Lists processes with their credentials""" + + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + ] + + def _generator(self, tasks): + kernel = self.context.modules[self.config["kernel"]] + + for task in tasks: + task_pid = task.p_pid + task_comm = utility.array_to_string(task.p_comm) + task_cred = task.p_ucred + task_umask = '{0:03o}'.format(task.p_pd.pd_cmask) + task_flags = 'C' if (task_cred.cr_flags & 0x1) else '-' + + groups = kernel.object(object_type = "array", + offset = task.p_ucred.cr_groups, + count = task.p_ucred.cr_ngroups, + subtype = kernel.get_type("int")) + task_groups = ','.join([str(group) for group in groups]) + + yield (0, (task_pid, task_comm, task_cred.cr_uid, task_cred.cr_ruid, task_cred.cr_svuid, groups[0], + task_cred.cr_rgid, task_cred.cr_svgid, task_umask, task_flags, task_groups)) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [("PID", int), ("COMM", str), ("EUID", int), ("RUID", int), ("SVUID", int), ("EGID", int), ("RGID", int), + ("SVGID", int), ("UMASK", str), ("FLAGS", str), ("GROUPS", str)], + self._generator(pslist.PsList.list_tasks(self.context, self.config["kernel"], filter_func = filter_func)), + ) diff --git a/volatility3/framework/plugins/freebsd/envars.py b/volatility3/framework/plugins/freebsd/envars.py new file mode 100644 index 000000000..04edcbb8f --- /dev/null +++ b/volatility3/framework/plugins/freebsd/envars.py @@ -0,0 +1,83 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import struct +from typing import Iterator, Tuple, Any, Generator, List + +from volatility3.framework import constants, exceptions, renderers, interfaces, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.freebsd import pslist + + +class Envars(plugins.PluginInterface): + """Lists processes with their environment variables""" + + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + ] + + def _generator(self, tasks): + kernel = self.context.modules[self.config["kernel"]] + is_64bit = symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + + for task in tasks: + task_pid = task.p_pid + task_comm = utility.array_to_string(task.p_comm) + task_path = task.p_textvp.get_vpath(kernel) + proc_layer_name = task.add_process_layer() + proc_layer = self._context.layers[proc_layer_name] + if is_64bit and task.p_sysent.sv_flags & 0x100 == 0x100 and task.p_vmspace.has_member( + "vm_stacktop") and task.p_vmspace.vm_stacktop != 0: + # SV_ILP32 on 64-bit + ps_strings = self.context.object(kernel.symbol_table_name + constants.BANG + "freebsd32_ps_strings", + layer_name = proc_layer_name, + offset = task.p_vmspace.vm_stacktop - task.p_sysent.sv_psstringssz) + nenvstr = ps_strings.ps_nenvstr + for i in range(nenvstr): + vector = struct.unpack(" List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + ] + + @classmethod + def list_klds(cls, context: interfaces.context.ContextInterface, kernel_module_name: str): + """Lists all the klds in the primary layer. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the the kernel module on which to operate + + Yields: + Klds + """ + kernel = context.modules[kernel_module_name] + kernel_layer = context.layers[kernel.layer_name] + + linker_files = kernel.object_from_symbol(symbol_name = "linker_files") + linker_file = linker_files.tqh_first + + while linker_file != 0: + yield linker_file.dereference() + linker_file = linker_file.link.tqe_next + + def _generator(self): + for kld in self.list_klds(self.context, self.config["kernel"]): + kld_id = kld.id + kld_refs = kld.refs + kld_size = kld.size + kld_address = kld.address + kld_pathname = utility.pointer_to_string(kld.pathname, 1024) + + yield 0, (format_hints.Hex(kld.vol.offset), kld_id, kld_refs, format_hints.Hex(kld_size), + format_hints.Hex(kld_address), kld_pathname) + + def run(self): + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Id", int), ("Refs", int), ("Size", format_hints.Hex), + ("Address", format_hints.Hex), ("Name", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/freebsd/lsmod.py b/volatility3/framework/plugins/freebsd/lsmod.py new file mode 100644 index 000000000..843c2234e --- /dev/null +++ b/volatility3/framework/plugins/freebsd/lsmod.py @@ -0,0 +1,66 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""A module containing a collection of plugins that produce data typically +found in Freebsd's kldstat -v command.""" + +from typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints + + +class LsMod(plugins.PluginInterface): + """Lists loaded kernel modules.""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + ] + + @classmethod + def list_modules(cls, context: interfaces.context.ContextInterface, kernel_module_name: str): + """Lists all the modules in the primary layer. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the the kernel module on which to operate + + Yields: + Modules + """ + kernel = context.modules[kernel_module_name] + kernel_layer = context.layers[kernel.layer_name] + + modules = kernel.object_from_symbol(symbol_name = "modules") + module = modules.tqh_first + + while module != 0: + yield module.dereference() + module = module.link.tqe_next + + def _generator(self): + for module in self.list_modules(self.context, self.config["kernel"]): + module_id = module.id + module_name = utility.pointer_to_string(module.name, 32) + module_file_pathname = utility.pointer_to_string(module.file.pathname, 1024) + + yield 0, (format_hints.Hex(module.vol.offset), module_id, module_name, module_file_pathname) + + def run(self): + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Id", int), ("Name", str), ("Kld", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/freebsd/lsof.py b/volatility3/framework/plugins/freebsd/lsof.py new file mode 100644 index 000000000..9fb5eef05 --- /dev/null +++ b/volatility3/framework/plugins/freebsd/lsof.py @@ -0,0 +1,55 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging + +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols import freebsd +from volatility3.plugins.freebsd import pslist + +vollog = logging.getLogger(__name__) + + +class Lsof(plugins.PluginInterface): + """Lists all open file descriptors for all processes.""" + + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.VersionRequirement(name = "freebsdutils", + component = freebsd.FreebsdUtilities, + version = (1, 0, 0)), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + ] + + def _generator(self, tasks): + kernel = self.context.modules[self.config["kernel"]] + for task in tasks: + pid = task.p_pid + + for _, filepath, fd in freebsd.FreebsdUtilities.files_descriptors_for_process(self.context, kernel, task): + yield (0, (pid, fd, filepath)) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [("PID", int), ("File Descriptor", int), ("File Path", str)], + self._generator(pslist.PsList.list_tasks(self.context, self.config["kernel"], filter_func = filter_func)), + ) diff --git a/volatility3/framework/plugins/freebsd/mount.py b/volatility3/framework/plugins/freebsd/mount.py new file mode 100644 index 000000000..d2fa4b95a --- /dev/null +++ b/volatility3/framework/plugins/freebsd/mount.py @@ -0,0 +1,66 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""A module containing a collection of plugins that produce data typically +found in Freebsd's mount command.""" + +from typing import List + +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints + + +class Mount(plugins.PluginInterface): + """Lists mounted file systems.""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + ] + + @classmethod + def list_mounts(cls, context: interfaces.context.ContextInterface, kernel_module_name: str): + """Lists all the mounted file systems in the primary layer. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the the kernel module on which to operate + + Yields: + Mounted file systems + """ + kernel = context.modules[kernel_module_name] + kernel_layer = context.layers[kernel.layer_name] + + mountlist = kernel.object_from_symbol(symbol_name = "mountlist") + mount = mountlist.tqh_first + + while mount != 0: + yield mount.dereference() + mount = mount.mnt_list.tqe_next + + def _generator(self): + for mount in self.list_mounts(self.context, self.config["kernel"]): + mount_mntfromname = utility.array_to_string(mount.mnt_stat.f_mntfromname) + mount_mntonname = utility.array_to_string(mount.mnt_stat.f_mntonname) + mount_fstypename = utility.array_to_string(mount.mnt_stat.f_fstypename) + + yield 0, (format_hints.Hex(mount.vol.offset), mount_mntfromname, mount_mntonname, mount_fstypename) + + def run(self): + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Special device", str), ("Mount point", str), ("Type", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/freebsd/proc_maps.py b/volatility3/framework/plugins/freebsd/proc_maps.py new file mode 100644 index 000000000..9152c68e1 --- /dev/null +++ b/volatility3/framework/plugins/freebsd/proc_maps.py @@ -0,0 +1,216 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.freebsd import pslist +from typing import Callable, Generator, Type, Optional +import logging + +vollog = logging.getLogger(__name__) + + +class Maps(interfaces.plugins.PluginInterface): + """Lists process memory ranges that potentially contain injected code.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + requirements.BooleanRequirement( + name = "dump", + description = "Extract listed memory segments", + default = False, + optional = True, + ), + requirements.ListRequirement( + name = "address", + description = "Process virtual memory addresses to include " + "(all other VMA sections are excluded). This can be any " + "virtual address within the VMA section. Virtual addresses " + "must be separated by a space.", + element_type = int, + optional = True, + ), + requirements.IntRequirement( + name = "maxsize", + description = "Maximum size for dumped VMA sections " + "(all the bigger sections will be ignored)", + default = cls.MAXSIZE_DEFAULT, + optional = True, + ), + ] + + @classmethod + def list_vmas( + cls, + task: interfaces.objects.ObjectInterface, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: True, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Lists the Virtual Memory Areas of a specific process. + + Args: + task: task object from which to list the vma + filter_func: Function to take a vma and return False if it should be filtered out + + Returns: + Yields vmas based on the task and filtered based on the filter function + """ + for vma in task.get_map_iter(): + if filter_func(vma): + yield vma + else: + vollog.debug(f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.p_pid} due to filter_func") + + @classmethod + def vma_dump( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + vm_start: int, + vm_end: int, + open_method: Type[interfaces.plugins.FileHandlerInterface], + maxsize: int = MAXSIZE_DEFAULT, + ) -> Optional[interfaces.plugins.FileHandlerInterface]: + """Extracts the complete data for VMA as a FileInterface. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + task: an task_struct instance + vm_start: The start virtual address from the vma to dump + vm_end: The end virtual address from the vma to dump + open_method: class to provide context manager for opening the file + maxsize: Max size of VMA section (default MAXSIZE_DEFAULT) + + Returns: + An open FileInterface object containing the complete data for the task or None in the case of failure + """ + pid = task.p_pid + + try: + proc_layer_name = task.add_process_layer() + except exceptions.InvalidAddressException as excp: + vollog.debug("Process {}: invalid address {} in layer {}".format(pid, excp.invalid_address, + excp.layer_name)) + return None + vm_size = vm_end - vm_start + + # check if vm_size is negative, this should never happen. + if vm_size < 0: + vollog.warning( + f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative.") + return None + # check if vm_size is larger than the maxsize limit, and therefore is not saved out. + if maxsize <= vm_size: + vollog.warning( + f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}" + ) + return None + proc_layer = context.layers[proc_layer_name] + file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp" + try: + file_handle = open_method(file_name) + chunk_size = 1024 * 1024 * 10 + offset = vm_start + while offset < vm_start + vm_size: + to_read = min(chunk_size, vm_start + vm_size - offset) + data = proc_layer.read(offset, to_read, pad = True) + file_handle.write(data) + offset += to_read + except Exception as excp: + vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}") + return None + return file_handle + + def _generator(self, tasks): + address_list = self.config.get("address", None) + if not address_list: + # do not filter as no address_list was supplied + vma_filter_func = lambda _: True + else: + # filter for any vm_start that matches the supplied address config + def vma_filter_function(task: interfaces.objects.ObjectInterface) -> bool: + addrs_in_vma = [addr for addr in address_list if task.start <= addr <= task.end] + + # if any of the user supplied addresses would fall within this vma return true + return bool(addrs_in_vma) + + vma_filter_func = vma_filter_function + + for task in tasks: + process_name = utility.array_to_string(task.p_comm) + process_pid = task.p_pid + + for vma in self.list_vmas(task, filter_func = vma_filter_func): + try: + vm_start = vma.start + vm_end = vma.end + except AttributeError: + vollog.debug( + f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {process_pid}") + continue + + path = vma.get_path(self.context.modules[self.config["kernel"]]) + + file_output = "Disabled" + if self.config["dump"]: + file_output = "Error outputting file" + file_handle = self.vma_dump( + self.context, + task, + vm_start, + vm_end, + self.open, + self.config["maxsize"], + ) + + if file_handle: + file_handle.close() + file_output = file_handle.preferred_filename + + yield ( + 0, + ( + process_pid, + process_name, + format_hints.Hex(vm_start), + format_hints.Hex(vm_end), + vma.get_perms(), + path, + file_output, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Protection", str), + ("Map Name", str), + ("File output", str), + ], + self._generator(pslist.PsList.list_tasks(self.context, self.config["kernel"], filter_func = filter_func)), + ) diff --git a/volatility3/framework/plugins/freebsd/psaux.py b/volatility3/framework/plugins/freebsd/psaux.py new file mode 100644 index 000000000..254dbb478 --- /dev/null +++ b/volatility3/framework/plugins/freebsd/psaux.py @@ -0,0 +1,83 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import struct +from typing import Iterator, Tuple, Any, Generator, List + +from volatility3.framework import constants, exceptions, renderers, interfaces, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.freebsd import pslist + + +class PsAux(plugins.PluginInterface): + """Lists processes with their command line arguments""" + + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + ] + + def _generator(self, tasks): + kernel = self.context.modules[self.config["kernel"]] + is_64bit = symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + + for task in tasks: + args: List[bytes] = [] + task_pid = task.p_pid + task_comm = utility.array_to_string(task.p_comm) + task_path = task.p_textvp.get_vpath(kernel) + proc_layer_name = task.add_process_layer() + proc_layer = self._context.layers[proc_layer_name] + if is_64bit and task.p_sysent.sv_flags & 0x100 == 0x100 and task.p_vmspace.has_member( + "vm_stacktop") and task.p_vmspace.vm_stacktop != 0: + # SV_ILP32 on 64-bit + ps_strings = self.context.object(kernel.symbol_table_name + constants.BANG + "freebsd32_ps_strings", + layer_name = proc_layer_name, + offset = task.p_vmspace.vm_stacktop - task.p_sysent.sv_psstringssz) + nargvstr = ps_strings.ps_nargvstr + for i in range(nargvstr): + vector = struct.unpack(" List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name = "pid", + description = "Filter on specific process IDs", + element_type = int, + optional = True, + ), + ] + + @classmethod + def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[Any], bool]: + """Constructs a filter function for process IDs. + + Args: + pid_list: List of process IDs that are acceptable (or None if all are acceptable) + + Returns: + Function which, when provided a process object, returns True if the process is to be filtered out of the list + """ + # FIXME: mypy #4973 or #2608 + pid_list = pid_list or [] + filter_list = [x for x in pid_list if x is not None] + if filter_list: + + def filter_func(x): + return x.p_pid not in filter_list + + return filter_func + else: + return lambda _: False + + def _generator(self): + for task in self.list_tasks( + self.context, + self.config["kernel"], + filter_func = self.create_pid_filter(self.config.get("pid", None)), + ): + offset = format_hints.Hex(task.vol.offset) + comm = utility.array_to_string(task.p_comm) + pid = task.p_pid + ppid = 0 + if task.p_pptr != 0: + ppid = task.p_pptr.p_pid + + yield (0, (offset, pid, ppid, comm)) + + @classmethod + def list_tasks( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[Any], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Lists all the processes in the primary layer + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + kernel_module_name: The name of the the kernel module on which to operate + filter_func: A function which takes a process object and returns True if the process should be ignored/filtered + + Yields: + Process objects + """ + + kernel = context.modules[kernel_module_name] + + proc = kernel.object_from_symbol(symbol_name = "allproc").lh_first.dereference() + + while proc.vol.offset != 0: + if not filter_func(proc): + yield proc + proc = proc.p_list.le_next.dereference() + + def run(self): + return renderers.TreeGrid( + [ + ("OFFSET (V)", format_hints.Hex), + ("PID", int), + ("PPID", int), + ("COMM", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/freebsd/pstree.py b/volatility3/framework/plugins/freebsd/pstree.py new file mode 100644 index 000000000..d545813d8 --- /dev/null +++ b/volatility3/framework/plugins/freebsd/pstree.py @@ -0,0 +1,74 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import List + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.freebsd import pslist + + +class PsTree(plugins.PluginInterface): + """Plugin for listing processes in a tree based on their parent process + ID.""" + + _required_framework_version = (2, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._processes = {} + self._levels = {} + self._children = {} + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name = "kernel", + description = "Kernel module for the OS", + architectures = ["Intel32", "Intel64"], + ), + requirements.PluginRequirement(name = "pslist", plugin = pslist.PsList, version = (1, 0, 0)), + ] + + def _find_level(self, pid): + """Finds how deep the pid is in the processes list.""" + seen = set([]) + seen.add(pid) + level = 0 + proc = self._processes.get(pid, None) + while proc is not None and proc.p_pptr != 0 and proc.p_pptr.p_pid not in seen: + ppid = int(proc.p_pptr.p_pid) + child_list = self._children.get(ppid, set([])) + child_list.add(proc.p_pid) + self._children[ppid] = child_list + proc = self._processes.get(ppid, None) + level += 1 + self._levels[pid] = level + + def _generator(self): + """Generates the tree list of processes""" + for proc in pslist.PsList.list_tasks(self.context, self.config["kernel"]): + self._processes[proc.p_pid] = proc + + # Build the child/level maps + for pid in self._processes: + self._find_level(pid) + + def yield_processes(pid): + proc = self._processes[pid] + row = (proc.p_pid, proc.p_pptr.p_pid, utility.array_to_string(proc.p_comm)) + + yield (self._levels[pid] - 1, row) + for child_pid in self._children.get(pid, []): + yield from yield_processes(child_pid) + + for pid in self._levels: + if self._levels[pid] == 1: + yield from yield_processes(pid) + + def run(self): + return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str)], self._generator()) diff --git a/volatility3/framework/symbols/freebsd/__init__.py b/volatility3/framework/symbols/freebsd/__init__.py new file mode 100644 index 000000000..576391a36 --- /dev/null +++ b/volatility3/framework/symbols/freebsd/__init__.py @@ -0,0 +1,74 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import constants, interfaces, objects +from volatility3.framework.objects import utility +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.freebsd import extensions + + +class FreebsdKernelIntermedSymbols(intermed.IntermediateSymbolTable): + provides = {"type": "interface"} + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.set_type_class("proc", extensions.proc) + self.set_type_class("vm_map_entry", extensions.vm_map_entry) + self.set_type_class("vnode", extensions.vnode) + + +class FreebsdUtilities(interfaces.configuration.VersionableInterface): + """Class with multiple useful freebsd functions.""" + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + @classmethod + def files_descriptors_for_process( + cls, + context: interfaces.context.ContextInterface, + kernel, + task: interfaces.objects.ObjectInterface, + ): + """Creates a generator for the file descriptors of a process + + Args: + kernel: + context: + task: The process structure to enumerate file descriptors from + + Return: + A 3 element tuple is yielded for each file descriptor: + 1) The file's object + 2) The path referenced by the descriptor. + The path is either empty, the full path of the file in the file system, or the formatted name for sockets, pipes, etc. + 3) The file descriptor number + """ + num_fds = task.p_fd.fd_files.fdt_nfiles + table_addr = task.p_fd.fd_files.fdt_ofiles.vol.offset + + fds = kernel.object( + object_type = "array", + offset = table_addr, + count = num_fds, + subtype = kernel.get_type("filedescent"), + ) + + for fd_num, f in enumerate(fds): + if f.fde_file and f.fde_file.f_type: + if f.fde_file.f_type == 1: # DTYPE_VNODE + vnode = f.fde_file.f_vnode + # XXX there seems to be a bug in enumerations, we can't get vnode.v_type.. + path = vnode.get_vpath(kernel) + if not path and vnode.v_rdev: + path = utility.array_to_string(vnode.v_rdev.si_name) + if not path: + path = "-" + elif f.fde_file.f_type == 2: # DTYPE_SOCKET + socket = f.fde_file.f_data.dereference().cast("socket") + path = f"" + else: + path = f"" + + yield f.fde_file, path, fd_num diff --git a/volatility3/framework/symbols/freebsd/extensions/__init__.py b/volatility3/framework/symbols/freebsd/extensions/__init__.py new file mode 100644 index 000000000..739cef54f --- /dev/null +++ b/volatility3/framework/symbols/freebsd/extensions/__init__.py @@ -0,0 +1,143 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +import logging +from typing import Iterable, Optional + +from volatility3.framework import interfaces, objects +from volatility3.framework.symbols import generic + +vollog = logging.getLogger(__name__) + + +class proc(generic.GenericIntelProcess): + + def add_process_layer(self, config_prefix: str = None, preferred_name: str = None) -> Optional[str]: + """Constructs a new layer based on the process's DTB. + + Returns the name of the Layer or None. + """ + parent_layer = self._context.layers[self.vol.layer_name] + + if not isinstance(parent_layer, interfaces.layers.TranslationLayerInterface): + raise TypeError("Parent layer is not a translation layer, unable to construct process layer") + + dtb = None + pmap = self.p_vmspace.vm_pmap + # Freebsd amd64 + if pmap.has_member("pm_ucr3") and pmap.pm_ucr3 != 0xffffffffffffffff: + dtb = pmap.pm_ucr3 + elif pmap.has_member("pm_cr3"): + dtb = pmap.pm_cr3 + # Freebsd i386 + elif pmap.has_member("pm_pdir"): + dtb, _ = parent_layer.translate(pmap.pm_pdir) + # Freebsd i386 with PAE + elif pmap.has_member("pm_pdpt"): + dtb, _ = parent_layer.translate(pmap.pm_pdpt) + # Freebsd i386 after merge of PAE and non-PAE pmaps into same kernel + elif pmap.has_member("pm_pdpt_pae") and pmap.pm_pdpt_pae: + dtb, _ = parent_layer.translate(pmap.pm_pdpt_pae) + elif pmap.has_member("pm_pdir_nopae") and pmap.pm_pdir_nopae: + dtb, _ = parent_layer.translate(pmap.pm_pdir_nopae) + + if not dtb: + return None + + if preferred_name is None: + preferred_name = self.vol.layer_name + f"_Process{self.p_pid}" + + # Add the constructed layer and return the name + return self._add_process_layer(self._context, dtb, config_prefix, preferred_name) + + def get_map_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: + if self.p_vmspace.vm_map.header.has_member("next"): + current_map = self.p_vmspace.vm_map.header.next + else: + current_map = self.p_vmspace.vm_map.header.right + + seen = set() # type: Set[int] + + for i in range(self.p_vmspace.vm_map.nentries): + if not current_map or current_map.vol.offset in seen: + break + + if current_map.eflags & 0x2 == 0: + # Skip MAP_ENTRY_IS_SUB_MAP + yield current_map + seen.add(current_map.vol.offset) + if current_map.has_member("next"): + current_map = current_map.next + else: + after = current_map.right + if after.left.start > current_map.start: + while True: + after = after.left + if after.left == current_map: + break + current_map = after + + +class vm_map_entry(objects.StructType): + + def get_perms(self): + permask = "rwx" + perms = "" + + for (ctr, i) in enumerate([1, 3, 5]): + if (self.protection & i) == i: + perms = perms + permask[ctr] + else: + perms = perms + "-" + + return perms + + def get_path(self, kernel): + vm_object = self.object.vm_object + + if vm_object == 0: + return '' + + while vm_object.backing_object != 0: + vm_object = vm_object.backing_object + + if vm_object.type != 2: # OBJT_VNODE + return '' + + vnode = vm_object.handle.dereference().cast('vnode') + return vnode.get_vpath(kernel) + + +class vnode(objects.StructType): + + def get_vpath(self, kernel): + """Lookup pathname of a vnode in the namecache""" + rootvnode = kernel.object_from_symbol(symbol_name = "rootvnode").dereference() + vp = self + components = list() + + while vp.vol.offset != 0: + if vp.vol.offset == rootvnode.vol.offset: + if len(components) == 0: + components.insert(0, '/') + else: + components.insert(0, '') + break + + if vp.v_vflag & 0x1 != 0: + # VV_ROOT set + vp = vp.v_mount.mnt_vnodecovered.dereference() + else: + ncp = vp.v_cache_dst.tqh_first + if ncp != 0: + ncn = ncp.nc_name.cast("string", max_length = ncp.nc_nlen) + components.insert(0, str(ncn)) + vp = ncp.nc_dvp.dereference() + else: + break + + if components: + return '/'.join(components) + else: + return '' diff --git a/volatility3/plugins/freebsd/__init__.py b/volatility3/plugins/freebsd/__init__.py new file mode 100644 index 000000000..8c4c3e0c2 --- /dev/null +++ b/volatility3/plugins/freebsd/__init__.py @@ -0,0 +1,19 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All Freebsd-related plugins. + +NOTE: This file is important for core plugins to run (which certain components such as the windows registry layers) +are dependent upon, please DO NOT alter or remove this file unless you know the consequences of doing so. + +The framework is configured this way to allow plugin developers/users to override any plugin functionality whether +existing or new. + +When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. +""" +import os +import sys + +# This is necessary to ensure the core plugins are available, whilst still be overridable +parent_module, module_name = ".".join(__name__.split(".")[:-1]), __name__.split(".")[-1] +__path__ = [os.path.join(x, module_name) for x in sys.modules[parent_module].__path__]