diff --git a/volatility/framework/symbols/linux/__init__.py b/volatility/framework/symbols/linux/__init__.py index f8d365294..e82c68d83 100644 --- a/volatility/framework/symbols/linux/__init__.py +++ b/volatility/framework/symbols/linux/__init__.py @@ -23,6 +23,12 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class('super_block', extensions.super_block) self.set_type_class('task_struct', extensions.task_struct) self.set_type_class('vm_area_struct', extensions.vm_area_struct) + self.set_type_class('qstr', extensions.qstr) + self.set_type_class('dentry', extensions.dentry) + self.set_type_class('fs_struct', extensions.fs_struct) + self.set_type_class('files_struct', extensions.files_struct) + self.set_type_class('vfsmount', extensions.vfsmount) + self.set_type_class('mount', extensions.mount) @classmethod def get_requirements(cls) -> typing.List[interfaces.configuration.RequirementInterface]: diff --git a/volatility/framework/symbols/linux/extensions/__init__.py b/volatility/framework/symbols/linux/extensions/__init__.py index 03a0f47b1..31db88866 100644 --- a/volatility/framework/symbols/linux/extensions/__init__.py +++ b/volatility/framework/symbols/linux/extensions/__init__.py @@ -2,14 +2,13 @@ import collections.abc import typing from volatility.framework import constants -from volatility.framework import objects, interfaces +from volatility.framework import exceptions, objects, interfaces from volatility.framework.objects import utility from volatility.framework.symbols import generic # Keep these in a basic module, to prevent import cycles when symbol providers require them - class task_struct(generic.GenericIntelProcess): def add_process_layer(self, config_prefix: str = None, @@ -33,6 +32,151 @@ class task_struct(generic.GenericIntelProcess): # Add the constructed layer and return the name return self._add_process_layer(self._context, dtb, config_prefix, preferred_name) + # based on __d_path from the Linux kernel + def _do_get_path(self, rdentry, rmnt, dentry, vfsmnt) -> str: + try: + rdentry.validate() + dentry.validate() + except InvalidDataException: + return "" + + ret_path = [] # type: typing.List[str] + + try: + inode = dentry.d_inode + ino = inode.i_ino + except exceptions.InvalidAddressException: + ino = 0 + + while dentry != rdentry or vfsmnt != rmnt: + dname = dentry.path() + if dname == "": + break + + ret_path.insert(0, dname.strip('/')) + if dentry == vfsmnt.get_mnt_root() or dentry == dentry.d_parent: + if vfsmnt.get_mnt_parent() == vfsmnt: + break + + dentry = vfsmnt.get_mnt_mountpoint() + vfsmnt = vfsmnt.get_mnt_parent() + + continue + + parent = dentry.d_parent + dentry = parent + + if ret_path == []: + return "" + + ret_val = '/'.join([str(p) for p in ret_path if p != ""]) + + if ret_val.startswith(("socket:", "pipe:")): + if ret_val.find("]") == -1: + ret_val = ret_val[:-1] + ":[{0}]".format(ino) + else: + ret_val = ret_val.replace("/", "") + + elif ret_val != "inotify": + ret_val = '/' + ret_val + + return ret_val + + # old method + def _get_path_file(self, filp) -> str: + rdentry = self.fs.get_root_dentry() + rmnt = self.fs.get_root_mnt() + dentry = filp.dentry + vfsmnt = filp.vfsmnt + + return self._do_get_path(rdentry, rmnt, dentry, vfsmnt) + + def _get_new_sock_pipe_path(self, filp, layer_name) -> str: + dentry = filp.dentry + + sym_addr = dentry.d_op.d_dname + + # BUG - ikelos please read + # layer.address_mask is currently a @propery so the code is awkward, such as: + # sym_addr = sym_addr | self._context.memory[layer_name].address_mask + # is there a reason it couldn't just be a normal function so address_mask(sym_addr) would work? + # the second issue is that the mask I am getting is 0x1ffffffff, when I really need it OR'd with 0xffffffff00000000 in order to get the correct value + # third - having to pass in 'layer_name' way from the plugin is pretty ugly, is there a better way to get access to the mask? + + # TODO - this is currently not ASLR aware, which makes the lookups fail, after the masking issue is fixed, test for ASLR handling + + symbols = list(self._context.symbol_space.get_symbols_by_location(sym_addr)) + if len(symbols) == 1: + sym = symbols[0].split("!")[1] + + if sym == "sockfs_dname": + pre_name = "socket" + + elif sym == "anon_inodefs_dname": + pre_name = "anon_inode" + + elif sym == "pipefs_dname": + pre_name = "pipe" + + elif sym == "simple_dname": + pre_name = self._get_path_file(filp) + + else: + pre_name = "".format(sym) + + ret = "{0}:[{1:d}]".format(pre_name, dentry.d_inode.i_ino) + + else: + ret = " {0:d}".format(sym_addr) + + return ret + + # a 'file' structure doesn't have enough information to properly restore its full path + # we need the root mount information from task_struct to determine this + def path_for_file(self, filp, layer_name) -> str: + try: + dentry = filp.dentry + except exceptions.InvalidAddressException: + return "" + + if dentry == 0: + return "" + + dname_is_valid = False + + # TODO COMPARE THIS IN LSOF OUTPUT TO VOL2 + try: + if dentry.d_op and hasattr(dentry.d_op, "d_dname") and dentry.d_op.d_dname: + dname_is_valid = True + + except exceptions.InvalidAddressException: + dname_is_valid = False + + if dname_is_valid: + ret = self._get_new_sock_pipe_path(filp, layer_name) + else: + ret = self._get_path_file(filp) + + return ret + +class fs_struct(objects.Struct): + def get_root_dentry(self): + # < 2.6.26 + if hasattr(self, "rootmnt"): + return self.root + else: + return self.root.dentry + + raise AttributeError("Unable to find the root dentry") + + def get_root_mnt(self): + # < 2.6.26 + if hasattr(self, "rootmnt"): + return self.rootmnt + else: + return self.root.mnt + + raise AttributeError("Unable to find the root mount") class mm_struct(objects.Struct): @property @@ -52,7 +196,6 @@ class mm_struct(objects.Struct): seen.add(link.vol.offset) link = link.vm_next - class super_block(objects.Struct): # include/linux/kdev_t.h MINORBITS = 20 @@ -65,51 +208,111 @@ class super_block(objects.Struct): def minor(self) -> int: return self.s_dev & ((1 << self.MINORBITS) - 1) - class vm_area_struct(objects.Struct): - # include/linux/mm.h - VM_READ = 0x00000001 - VM_WRITE = 0x00000002 - VM_EXEC = 0x00000004 + perm_flags = { + 0x00000001 : "r", + 0x00000002 : "w", + 0x00000004 : "x", + } - @property - def flags(self) -> str: - """Returns an rwx string representation of the flags in a vm_area_struct.""" + extended_flags = { + 0x00000001 : "VM_READ", + 0x00000002 : "VM_WRITE", + 0x00000004 : "VM_EXEC", + 0x00000008 : "VM_SHARED", + 0x00000010 : "VM_MAYREAD", + 0x00000020 : "VM_MAYWRITE", + 0x00000040 : "VM_MAYEXEC", + 0x00000080 : "VM_MAYSHARE", + 0x00000100 : "VM_GROWSDOWN", + 0x00000200 : "VM_NOHUGEPAGE", + 0x00000400 : "VM_PFNMAP", + 0x00000800 : "VM_DENYWRITE", + 0x00001000 : "VM_EXECUTABLE", + 0x00002000 : "VM_LOCKED", + 0x00004000 : "VM_IO", + 0x00008000 : "VM_SEQ_READ", + 0x00010000 : "VM_RAND_READ", + 0x00020000 : "VM_DONTCOPY", + 0x00040000 : "VM_DONTEXPAND", + 0x00080000 : "VM_RESERVED", + 0x00100000 : "VM_ACCOUNT", + 0x00200000 : "VM_NORESERVE", + 0x00400000 : "VM_HUGETLB", + 0x00800000 : "VM_NONLINEAR", + 0x01000000 : "VM_MAPPED_COP__VM_HUGEPAGE", + 0x02000000 : "VM_INSERTPAGE", + 0x04000000 : "VM_ALWAYSDUMP", + 0x08000000 : "VM_CAN_NONLINEAR", + 0x10000000 : "VM_MIXEDMAP", + 0x20000000 : "VM_SAO", + 0x40000000 : "VM_PFN_AT_MMAP", + 0x80000000 : "VM_MERGEABLE", + } + + def _parse_flags(self, vm_flags, parse_flags) -> str: + """Returns an string representation of the flags in a vm_area_struct.""" retval = "" - vm_flags = self.vm_flags - for (bit, char) in ((self.VM_READ, 'r'), (self.VM_WRITE, 'w'), (self.VM_EXEC, 'x')): - if (vm_flags & bit) == bit: + + for mask, char in parse_flags.items(): + if (vm_flags & mask) == mask: retval = retval + char else: retval = retval + '-' return retval + # only parse the rwx bits + def protection(self) -> str: + return self._parse_flags(self.vm_flags & 0b1111, vm_area_struct.perm_flags) + + # used by malfind + def flags(self) -> str: + return self._parse_flags(self.vm_flags, extended_flags) + def page_offset(self) -> int: if self.vm_file == 0: return 0 return self.vm_pgoff << constants.linux.PAGE_SHIFT +class qstr(objects.Struct): + def name_as_str(self) -> str: + if hasattr(self, "len"): + str_length = self.len + else: + str_length = 255 + + try: + ret = utility.pointer_to_string(self.name, str_length) + except exceptions.InvalidAddressException: + ret = "" + + return ret + +class dentry(objects.Struct): + def path(self) -> str: + return self.d_name.name_as_str() class struct_file(objects.Struct): @property - def full_path(self) -> str: - parts = [] # type: typing.List[str] - path = self.f_path - path_dentry = path.dentry - seen = set() # type: typing.Set[int] - while path_dentry != 0 and path_dentry.vol.offset not in seen: - name = utility.pointer_to_string(path_dentry.d_name.name, path_dentry.d_name.len) - if name == "/": - break - parts.insert(0, name) - seen.add(path_dentry.vol.offset) - path_dentry = path_dentry.d_parent + def dentry(self) -> interfaces.objects.ObjectInterface: + if hasattr(self, "f_dentry"): + return self.f_dentry + else: + return self.f_path.dentry + + raise AttributeError("Unable to find file -> dentry") - return "/" + "/".join(parts) + @property + def vfsmnt(self) -> interfaces.objects.ObjectInterface: + if hasattr(self, "f_vfsmnt"): + return self.f_vfsmnt + else: + return self.f_path.mnt + raise AttributeError("Unable to find file -> vfs mount") class list_head(objects.Struct, collections.abc.Iterable): def to_list(self, @@ -142,3 +345,89 @@ class list_head(objects.Struct, collections.abc.Iterable): def __iter__(self) -> typing.Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) + +class files_struct(objects.Struct): + def get_fds(self) -> interfaces.objects.ObjectInterface: + if hasattr(self, "fdt"): + return self.fdt.fd.dereference() + else: + return self.fd.dereference() + + raise AttributeError("Unable to find files -> file descriptors") + + def get_max_fds(self) -> interfaces.objects.ObjectInterface: + if hasattr(self, "fdt"): + return self.fdt.max_fds + else: + return self.max_fds + + raise AttributeError("Unable to find files -> maximum file descriptors") + +class mount(objects.Struct): + + def get_mnt_sb(self): + if hasattr(self, "mnt"): + return self.mnt.mnt_sb + else: + return self.mnt_sb + + raise AttributeError("Unable to find mount -> super block") + + def get_mnt_root(self): + if hasattr(self, "mnt"): + return self.mnt.mnt_root + else: + return self.mnt_root + + raise AttributeError("Unable to find mount -> mount root") + + def get_mnt_flags(self): + if hasattr(self, "mnt"): + return self.mnt.mnt_flags + else: + return self.mnt_flags + + raise AttributeError("Unable to find mount -> mount flags") + + def get_mnt_parent(self): + return self.mnt_parent + + def get_mnt_mountpoint(self): + return self.mnt_mountpoint + +class vfsmount(objects.Struct): + def is_valid(self): + return self.get_mnt_sb() != 0 and \ + self.get_mnt_root() != 0 and \ + self.get_mnt_parent() != 0 + + def _get_real_mnt(self): + table_name = self.vol.type_name.split(constants.BANG)[0] + + mount_struct = "{0}{1}mount".format(table_name, constants.BANG) + + offset = self._context.symbol_space.get_type(mount_struct).relative_child_offset("mnt") + + return self._context.object(mount_struct, self.vol.layer_name, offset = self.vol.offset - offset) + + def get_mnt_parent(self): + if hasattr(self, "mnt_parent"): + return self.mnt_parent + else: + return self._get_real_mnt().mnt_parent + + raise AttributeError("Unable to find vfs mount -> mount parent") + + def get_mnt_mountpoint(self): + if hasattr(self, "mnt_mountpoint"): + return self.mnt_mountpoint + else: + return self._get_real_mnt().mnt_mountpoint + + raise AttributeError("Unable to find vfs mount -> mount point") + + def get_mnt_root(self): + return self.mnt_root + + + diff --git a/volatility/plugins/linux/lsof.py b/volatility/plugins/linux/lsof.py new file mode 100644 index 000000000..1be6607fd --- /dev/null +++ b/volatility/plugins/linux/lsof.py @@ -0,0 +1,63 @@ +"""A module containing a collection of plugins that produce data +typically found in Linux's /proc file system. +""" +import logging + +from volatility.framework.interfaces import plugins +from volatility.framework import renderers +from volatility.framework import constants +from volatility.framework.automagic import linux +from volatility.framework.renderers import format_hints +from volatility.framework.objects import utility +from volatility.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + +class Lsof(plugins.PluginInterface): + """Lists all memory maps for all processes""" + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return pslist.PsList.get_requirements() + [] + + # yields list of data, e.g.: calculate + def _generator(self, tasks): + layer_name = self.config['primary.memory_layer'] + + _, aslr_shift = linux.LinuxUtilities.find_aslr(self.context, self.config["vmlinux"], layer_name) + vmlinux = self.context.module(self.config["vmlinux"], self.config["primary"], aslr_shift) + pointer_template = self.context.symbol_space[self.config['vmlinux']].get_type('pointer') + + for task in tasks: + fd_table = task.files.get_fds() + if fd_table == 0: + continue + + max_fds = task.files.get_max_fds() + + proc_name = utility.array_to_string(task.comm) + + # corruption check + if max_fds > 500000: + continue + + fds = vmlinux.object(type_name="array", offset = fd_table.vol.offset, subtype = pointer_template, count = max_fds) + + for (i, fd_ptr) in enumerate(fds): + if fd_ptr: + filp = fd_ptr.dereference().cast(self.config["vmlinux"] + constants.BANG + 'file') + + full_path = task.path_for_file(filp, layer_name) + + yield (0, (task.pid, proc_name, i, full_path)) + + def run(self): + plugin = pslist.PsList(self.context, "plugins.Lsof") + + return renderers.TreeGrid( + [("PID", int), + ("Process", str), + ("FD", int), + ("Path", str)], + self._generator(plugin.list_tasks())) diff --git a/volatility/plugins/linux/proc.py b/volatility/plugins/linux/proc.py index 5ad4b164a..c15ac902f 100644 --- a/volatility/plugins/linux/proc.py +++ b/volatility/plugins/linux/proc.py @@ -30,7 +30,7 @@ class Maps(plugins.PluginInterface): name = utility.array_to_string(task.comm) for vma in task.mm.mmap_iter: - flags = vma.flags + flags = vma.protection() page_offset = vma.page_offset() major = 0 minor = 0 @@ -42,7 +42,8 @@ class Maps(plugins.PluginInterface): major = inode_object.i_sb.major minor = inode_object.i_sb.minor inode = inode_object.i_ino - path = vma.vm_file.full_path + # TODO - update the second parameter to hopefully go away once extension is updated + path = task.path_for_file(vma.vm_file, "") yield ( 0,