mirror of
https://github.com/volatilityfoundation/volatility
synced 2026-06-08 18:04:46 +00:00
294 lines
12 KiB
Python
294 lines
12 KiB
Python
# Volatility
|
|
# Copyright (C) 2008-2013 Volatility Foundation
|
|
#
|
|
# This file is part of Volatility.
|
|
#
|
|
# Volatility 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.
|
|
#
|
|
# Volatility 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 Volatility. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import textwrap
|
|
import volatility.debug as debug
|
|
import volatility.fmtspec as fmtspec
|
|
import volatility.obj as obj
|
|
import volatility.registry as registry
|
|
import volatility.renderers as renderers
|
|
import volatility.addrspace as addrspace
|
|
from volatility.renderers.basic import Address, Address64, Hex
|
|
from volatility.renderers.dot import DotRenderer
|
|
from volatility.renderers.html import HTMLRenderer, JSONRenderer
|
|
from volatility.renderers.sqlite import SqliteRenderer
|
|
from volatility.renderers.text import TextRenderer, FormatCellRenderer, QuickTextRenderer
|
|
from volatility.renderers.xlsx import XLSXRenderer
|
|
|
|
|
|
class Command(object):
|
|
""" Base class for each plugin command """
|
|
op = ""
|
|
opts = ""
|
|
args = ""
|
|
cmdname = ""
|
|
# meta_info will be removed
|
|
meta_info = {}
|
|
# Make these class variables so they can be modified across every plugin
|
|
elide_data = True
|
|
tablesep = " "
|
|
text_sort_column = None
|
|
|
|
def __init__(self, config, *_args, **_kwargs):
|
|
""" Constructor uses args as an initializer. It creates an instance
|
|
of OptionParser, populates the options, and finally parses the
|
|
command line. Options are stored in the self.opts attribute.
|
|
"""
|
|
self._config = config
|
|
self._formatlist = []
|
|
|
|
@staticmethod
|
|
def register_options(config):
|
|
"""Registers options into a config object provided"""
|
|
config.add_option("OUTPUT", default = 'text',
|
|
cache_invalidator = False,
|
|
help = "Output in this format (format support is module specific)")
|
|
|
|
config.add_option("OUTPUT-FILE", default = None,
|
|
cache_invalidator = False,
|
|
help = "write output in this file")
|
|
|
|
config.add_option("VERBOSE", default = 0, action = 'count',
|
|
cache_invalidator = False,
|
|
short_option = 'v', help = 'Verbose information')
|
|
|
|
@classmethod
|
|
def help(cls):
|
|
""" This function returns a string that will be displayed when a
|
|
user lists available plugins.
|
|
"""
|
|
try:
|
|
return textwrap.dedent(cls.__doc__)
|
|
except (AttributeError, TypeError):
|
|
return ""
|
|
|
|
@staticmethod
|
|
def is_valid_profile(profile):
|
|
return True
|
|
|
|
def calculate(self):
|
|
""" This function is responsible for performing all calculations
|
|
|
|
We should not have any output functions (e.g. print) in this
|
|
function at all.
|
|
|
|
If this function is expected to take a long time to return
|
|
some data, the function should return a generator.
|
|
"""
|
|
|
|
|
|
def execute(self):
|
|
""" Executes the plugin command."""
|
|
# Check we can support the plugins
|
|
profs = registry.get_plugin_classes(obj.Profile)
|
|
# force user to give a profile if a plugin
|
|
# other than kdbgscan or imageinfo are given:
|
|
if self.__class__.__name__.lower() in ["kdbgscan", "imageinfo"] and self._config.PROFILE == None:
|
|
self._config.update("PROFILE", "WinXPSP2x86")
|
|
elif self._config.PROFILE == None:
|
|
debug.error("You must set a profile!")
|
|
if self._config.PROFILE not in profs:
|
|
debug.error("Invalid profile " + self._config.PROFILE + " selected")
|
|
if not self.is_valid_profile(profs[self._config.PROFILE]()):
|
|
debug.error("This command does not support the profile " + self._config.PROFILE)
|
|
|
|
# # Executing plugins is done in two stages - first we calculate
|
|
data = self.calculate()
|
|
|
|
## Then we render the result in some way based on the
|
|
## requested output mode:
|
|
function_name = "render_{0}".format(self._config.OUTPUT)
|
|
if self._config.OUTPUT_FILE:
|
|
if os.path.exists(self._config.OUTPUT_FILE):
|
|
debug.error("File " + self._config.OUTPUT_FILE + " already exists. Cowardly refusing to overwrite it...")
|
|
outfd = open(self._config.OUTPUT_FILE, 'wb')
|
|
# TODO: We should probably check that this won't blat over an existing file
|
|
else:
|
|
outfd = sys.stdout
|
|
|
|
try:
|
|
func = getattr(self, function_name)
|
|
except AttributeError:
|
|
## Try to find out what formats are supported
|
|
result = []
|
|
for x in dir(self):
|
|
if x.startswith("render_"):
|
|
_a, b = x.split("_", 1)
|
|
result.append(b)
|
|
|
|
print "Plugin {0} is unable to produce output in format {1}. Supported formats are {2}. Please send a feature request".format(self.__class__.__name__, self._config.OUTPUT, result)
|
|
return
|
|
|
|
func(outfd, data)
|
|
|
|
def _formatlookup(self, profile, code):
|
|
"""Code to turn profile specific values into format specifications"""
|
|
code = code or ""
|
|
if not code.startswith('['):
|
|
return code
|
|
|
|
# Strip off the square brackets
|
|
code = code[1:-1].lower()
|
|
if code.startswith('addr'):
|
|
spec = fmtspec.FormatSpec("#10x")
|
|
if profile.metadata.get('memory_model', '32bit') == '64bit':
|
|
spec.minwidth += 8
|
|
if 'pad' in code:
|
|
spec.fill = "0"
|
|
spec.align = spec.align if spec.align else "="
|
|
else:
|
|
# Non-padded addresses will come out as numbers,
|
|
# so titles should align >
|
|
spec.align = ">"
|
|
return spec.to_string()
|
|
|
|
# Something went wrong
|
|
debug.warning("Unknown table format specification: " + code)
|
|
return ""
|
|
|
|
def _elide(self, string, length):
|
|
"""Adds three dots in the middle of a string if it is longer than length"""
|
|
# Only elide data if we've been asked to (which we are by default)
|
|
if not self.elide_data:
|
|
return string
|
|
|
|
if length == -1:
|
|
return string
|
|
if len(string) < length:
|
|
return (" " * (length - len(string))) + string
|
|
elif len(string) == length:
|
|
return string
|
|
else:
|
|
if length < 5:
|
|
debug.error("Cannot elide a string to length less than 5")
|
|
even = ((length + 1) % 2)
|
|
length = (length - 3) / 2
|
|
return string[:length + even] + "..." + string[-length:]
|
|
|
|
def format_value(self, value, fmt):
|
|
""" Formats an individual field using the table formatting codes"""
|
|
profile = addrspace.BufferAddressSpace(self._config).profile
|
|
return ("{0:" + self._formatlookup(profile, fmt) + "}").format(value)
|
|
|
|
def table_header(self, outfd, title_format_list = None):
|
|
"""Table header renders the title row of a table
|
|
|
|
This also stores the header types to ensure
|
|
everything is formatted appropriately.
|
|
It must be a list of tuples rather than a dict for ordering purposes.
|
|
"""
|
|
titles = []
|
|
rules = []
|
|
self._formatlist = []
|
|
profile = addrspace.BufferAddressSpace(self._config).profile
|
|
|
|
for (k, v) in title_format_list:
|
|
spec = fmtspec.FormatSpec(self._formatlookup(profile, v))
|
|
# If spec.minwidth = -1, this field is unbounded length
|
|
if spec.minwidth != -1:
|
|
spec.minwidth = max(spec.minwidth, len(k))
|
|
|
|
# Get the title specification to follow the alignment of the field
|
|
titlespec = fmtspec.FormatSpec(formtype = 's', minwidth = max(spec.minwidth, len(k)))
|
|
titlespec.align = spec.align if spec.align in "<>^" else "<"
|
|
|
|
# Add this to the titles, rules, and formatspecs lists
|
|
titles.append(("{0:" + titlespec.to_string() + "}").format(k))
|
|
rules.append("-" * titlespec.minwidth)
|
|
self._formatlist.append(spec)
|
|
|
|
# Write out the titles and line rules
|
|
if outfd:
|
|
outfd.write(self.tablesep.join(titles) + "\n")
|
|
outfd.write(self.tablesep.join(rules) + "\n")
|
|
|
|
def table_row(self, outfd, *args):
|
|
"""Outputs a single row of a table"""
|
|
reslist = []
|
|
if len(args) > len(self._formatlist):
|
|
debug.error("Too many values for the table")
|
|
for index in range(len(args)):
|
|
spec = self._formatlist[index]
|
|
result = self._elide(("{0:" + spec.to_string() + "}").format(args[index]), spec.minwidth)
|
|
reslist.append(result)
|
|
outfd.write(self.tablesep.join(reslist) + "\n")
|
|
|
|
text_stock_renderers = {Hex: "#x",
|
|
Address: "#8x",
|
|
Address64: "#16x",
|
|
int: "",
|
|
str: "<",
|
|
float: ".2",
|
|
bytes: ""}
|
|
|
|
|
|
def text_cell_renderers(self, columns):
|
|
"""Returns default renderers for the columns listed"""
|
|
renderlist = [FormatCellRenderer("")] * len(columns)
|
|
|
|
# FIXME: Really, this should be handled by the plugin knowing what type of AS each object comes from
|
|
# However, as a nasty workaround, we can force all x64 profiles to produce addresses that are 64-bit in length
|
|
# It does not deal with PAE address spaces, or WoW64 addresses, or anything else weird or wonderful
|
|
# This will NOT be in volatility 3.0
|
|
x64 = False
|
|
if self._config.PROFILE.endswith("x64"):
|
|
x64 = True
|
|
|
|
for column in columns:
|
|
if not isinstance(column, renderers.Column):
|
|
raise TypeError("Columns must be a list of Column objects")
|
|
columntype = column.type if not x64 or column.type != Address else Address64
|
|
renderlist[column.index] = FormatCellRenderer(self.text_stock_renderers[columntype])
|
|
return renderlist
|
|
|
|
def unified_output(self, data):
|
|
raise NotImplementedError("Rendering using the unified output format has not been implemented for this plugin.")
|
|
|
|
def _render(self, outfd, renderer, data):
|
|
output = self.unified_output(data)
|
|
|
|
if isinstance(output, renderers.TreeGrid):
|
|
renderer.render(outfd, output)
|
|
else:
|
|
raise TypeError("Unified Output must return a TreeGrid object")
|
|
|
|
def render_text(self, outfd, data):
|
|
self._render(outfd, TextRenderer(self.text_cell_renderers, sort_column = self.text_sort_column,
|
|
config = self._config), data)
|
|
|
|
def render_quicktext(self, outfd, data):
|
|
self._render(outfd, QuickTextRenderer(self.text_cell_renderers, sort_column = self.text_sort_column), data)
|
|
|
|
def render_json(self, outfd, data):
|
|
self._render(outfd, JSONRenderer(), data)
|
|
|
|
def render_sqlite(self, outfd, data):
|
|
self._render(outfd, SqliteRenderer(self.__class__.__name__, self._config), data)
|
|
|
|
def render_dot(self, outfd, data):
|
|
self._render(outfd, DotRenderer(self.text_cell_renderers, self._config), data)
|
|
|
|
def render_html(self, outfd, data):
|
|
self._render(outfd, HTMLRenderer(), data)
|
|
|
|
def render_xlsx(self, outfd, data):
|
|
self._render(outfd, XLSXRenderer(self.text_cell_renderers, self._config), data)
|