# pstotal # Copyright (C) 2014 Sue Stirrup # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' Rewrite of and enhancements to the SANS(tm) Institute's text based pstotal plugin based on Jesse Kornblum's original for Volatility 2.0. @author: Sue Stirrup @license: GNU General Public License 2.0 or later @contact: info@sans.org @organization: The SANS(tm) Institute Amendments + enhancements * Default behaviour to display complete list of processes (process scan) - Interesting column added to show processes hidden from pslist. * Command line option to display only processes hidden from process list (original behaviour) * Graphical visualisation option using Graphviz and .dot format via the command line added: - Command line option to display process command (graphical representation only) - Command line option to display process path name (graphical representation only) - Processes from prior boot rendered in light blue (with exit time before current boot or not available - Processes from prior boot rendered in medium blue (with exit time after current boot - Processes from current boot but hidden from pslist rendered in red - Suspected pid reuse rendered in yellow ''' import volatility.plugins.filescan as filescan import volatility.plugins.common as common import volatility.utils as utils import volatility.obj as obj import volatility.win32.tasks as tasks import pdb import re class pstotal(common.AbstractWindowsCommand): ''' Combination of pslist,psscan & pstree --output=dot gives graphical representation ''' def __init__(self, config,*args, **kwargs): common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs) config.add_option('SHORT', short_option = 'S', default = False, help = 'Interesting processes only', action = 'store_true') config.add_option('CMD', short_option = 'c', default = False, help = 'Display process command line. All {} removed', action = 'store_true') config.add_option('PATH', short_option = 'P', default = False, help = 'Display process image path', action = 'store_true') def render_text(self, outfd, data): processes = data[0] interest = data[1] outfd.write("Offset (P) Name PID PPID PDB Time created Time exited Interesting \n" + \ "---------- ---------------- ------ ------ ---------- ------------------------ ------------------------ ----------- \n") if interest[processes[eprocess].obj_offset] == 1: interesting = 'TRUE' else: interesting = ' ' outfd.write("0x{0:08x} {1:16} {2:6} {3:6} 0x{4:08x} {5:24} {6:24} {7:7}\n".format( processes[eprocess].obj_offset, processes[eprocess].ImageFileName, processes[eprocess].UniqueProcessId, processes[eprocess].InheritedFromUniqueProcessId, processes[eprocess].Pcb.DirectoryTableBase, processes[eprocess].CreateTime or '', processes[eprocess].ExitTime or '', interesting)) def render_dot(self, outfd, data): objects = set() links = set() proc_seen = set() procs_to_check = set() proc_times = {} processes = data[0] filling = data[1] cmdline = data[2] pathname = data[3] smssTime = ' ' # Obtain boot time for proc in processes: proc_name = processes[proc].ImageFileName ppid = processes[proc].InheritedFromUniqueProcessId processp = "%s" % (processes[proc].UniqueProcessId) proc_times[processp] = processes[proc].CreateTime if proc_name.find("System") == 0 and processes[proc].CreateTime: smssTime = processes[proc].CreateTime elif proc_name.find("smss.exe") == 0 and ppid == 4: smssTime = processes[proc].CreateTime for eprocess in processes: proc_offset = processes[eprocess].obj_offset parentp = "%s" % (processes.get(eprocess).InheritedFromUniqueProcessId) label = "{0} | offset (P)\\n0x{1:08x} | {2} | ".format(processes[eprocess].UniqueProcessId, proc_offset, processes[eprocess].ImageFileName) # Display process command line option if self._config.CMD : try: if not processes[eprocess].CreateTime < smssTime: s = "%s" % (cmdline[proc_offset]) s = s.replace('"', '') s = s.replace('\\', '\\\\') pos = s.find("csrss.exe") if pos > 0: pos = pos + 9 s = s[:pos] + "\\n (Run pstree to get command parameters)" pos = s.find("conhost.exe") if pos > 0: pos = pos + 11 s = s[:pos] + "\\n (Run pstree to get command parameters)" label += "command:\\n{0} | ".format(s or 'not available') label = label.replace('{', '') label = label.replace('}', '') except KeyError: pass # Display process path option if self._config.PATH : try: if not processes[eprocess].CreateTime < smssTime: s = "%s" % (pathname[proc_offset]) s = s.replace('"', '') s = s.replace('\\', '\\\\') pos = s.find("csrss.exe") if pos > 0: pos = pos + 9 s = s[:pos] + "\\n (Run pstree to get command parameters)" pos = s.find("conhost.exe") if pos > 0: pos = pos + 11 s = s[:pos] + "\\n (Run pstree to get command parameters)" label += "path:\\n{0} | ".format(s or 'not available') except KeyError: pass label += "created:\\n{0} |".format(processes[eprocess].CreateTime or 'not available') # Identify processes that have exited if processes[eprocess].ExitTime: label += "exited:\\n{0}".format(processes[eprocess].ExitTime) options = ' style = "filled" fillcolor = "lightgray" ' else: label += "running" options = '' # Identify processes that are 'hidden' and relate to the current boot if filling[proc_offset] == 1: options = ' style = "filled" fillcolor = "red" ' # Identify processes that are 'hidden' and relate to the previous boot if processes[eprocess].CreateTime < smssTime and processes[eprocess].UniqueProcessId != 4: options = ' style = "filled" fillcolor = "lightblue" ' if not processes[eprocess].ExitTime: label = label[:-7] label += "not available\\nprior boot" # Exit time is after current boot time elif processes[eprocess].ExitTime > smssTime: options = ' style = "filled" fillcolor = "darkblue" ' label = "{" + label + "}" # Sometimes windows creates duplicate process blocks - one in the doubly linked list and one scraped. We need to see both pid = "%s" % (processes[eprocess].UniqueProcessId) if pid in proc_seen: objects.add('pid{0}a [label="{1}" shape="record" {2}];\n'.format(processes[eprocess].UniqueProcessId, label, options)) links.add("pid{0} -> pid{1}a [];\n".format(processes[eprocess].InheritedFromUniqueProcessId, processes[eprocess].UniqueProcessId)) else: proc_seen.add(pid) if parentp in proc_times and (processes.get(eprocess).CreateTime < proc_times[parentp]): links.add("pid{0}r -> pid{1} [];\n".format(processes[eprocess].InheritedFromUniqueProcessId, processes[eprocess].UniqueProcessId)) parent = "%sr" % processes[eprocess].InheritedFromUniqueProcessId if not parent in proc_seen: proc_seen.add(parent) objects.add('pid{0} [label="pid{1}" shape="oval" style = "filled" fillcolor = "yellow" ];\n'.format(parent, parentp)) else: links.add("pid{0} -> pid{1} [];\n".format(processes[eprocess].InheritedFromUniqueProcessId, processes[eprocess].UniqueProcessId)) objects.add('pid{0} [label="{1}" shape="record" {2}];\n'.format(processes[eprocess].UniqueProcessId, label, options)) ## Now write the dot file outfd.write("digraph processtree { \ngraph [rankdir = \"TB\"];\n") for link in links: outfd.write(link) for item in objects: outfd.write(item) outfd.write("}") def calculate(self): eproc = {} found = {} cmdline = {} pathname = {} # Brute force search for eproc blocks in pool memory address_space = utils.load_as(self._config) for eprocess in filescan.PSScan(self._config).calculate(): eproc[eprocess.obj_offset] = eprocess found[eprocess.obj_offset] = 1 # Walking the active process list. # Remove any tasks we find here from the brute force search if the --short option is set. # Anything left is something which was hidden/terminated/of interest. address_space = utils.load_as(self._config) for task in tasks.pslist(address_space): phys = address_space.vtop(task.obj_offset) if phys in eproc: if self._config.SHORT : del eproc[phys] del found[phys] else: found[phys] = 0 # Grab command line and parameters peb = task.Peb if peb: cmdline[phys] = peb.ProcessParameters.CommandLine pathname[phys] = peb.ProcessParameters.ImagePathName ret = [eproc, found, cmdline, pathname] return ret