mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-13 21:27:39 +02:00
Merge branch 'volatilityfoundation:develop' into feature/mermaid
This commit is contained in:
+23
-1
@@ -9,7 +9,11 @@ Synopsis
|
||||
**volatility** [-h] [-c CONFIG] [--parallelism [{processes,threads,off}]]
|
||||
[-e EXTEND] [-p PLUGIN_DIRS] [-s SYMBOL_DIRS] [-v] [-l LOG]
|
||||
[-o OUTPUT_DIR] [-q] [-r RENDERER] [-f FILE]
|
||||
[--write-config] [--single-location SINGLE_LOCATION]
|
||||
[--write-config] [--save-config SAVE_CONFIG]
|
||||
[--clear-cache] [--cache-path CACHE_PATH]
|
||||
[--offline]
|
||||
[--single-location SINGLE_LOCATION]
|
||||
[--stackers [STACKERS ...]]
|
||||
[--single-swap-locations SINGLE_SWAP_LOCATIONS]
|
||||
<plugin> ...
|
||||
|
||||
@@ -98,6 +102,10 @@ Options
|
||||
attempt to build upon, and can be considered the input for the program.
|
||||
|
||||
--write-config
|
||||
*Deprecated*
|
||||
Use of `--write-config` has been deprecated, replaced by `--save-config`
|
||||
|
||||
--save-config
|
||||
This flag specifies that volatility should write or overwrite a file
|
||||
called config.json in the current directory. The file will contain
|
||||
the necessary JSON configuration to recreate the environment that the
|
||||
@@ -105,11 +113,25 @@ Options
|
||||
other plugins, but there's no guarantee that plugins use the same
|
||||
configuration options.
|
||||
|
||||
--clear-cache
|
||||
Clears out all short-term cached items.
|
||||
|
||||
--cache-path
|
||||
Change the default path used to store the cache.
|
||||
|
||||
--offline
|
||||
Do not search online for additional JSON files.
|
||||
Run offline mode (defaults to false) and for
|
||||
remote windows symbol tables, linux/mac banner repositories.
|
||||
|
||||
--single-location SINGLE_LOCATION
|
||||
This specifies a URL which will be downloaded if necessary, and built
|
||||
upon by the automagic and, since most plugins require a single memory
|
||||
image, can be considered the input for the program.
|
||||
|
||||
--stackers STACKERS
|
||||
Creates the list of stackers to use based on the config option.
|
||||
|
||||
--single-swap-locations SINGLE_SWAP_LOCATIONS
|
||||
A comma-separated list of swap files to be considered as part of the
|
||||
memory image specified by the single-location or file parameters.
|
||||
|
||||
@@ -26,7 +26,7 @@ except ImportError:
|
||||
|
||||
# Volatility must be findable in sys.path in order for collect_submodules to work
|
||||
# This adds the current working directory, which should usually do the trick
|
||||
sys.path.append(os.getcwd())
|
||||
sys.path.append(os.path.dirname(os.path.abspath(SPEC)))
|
||||
|
||||
vol_analysis = Analysis(['vol.py'],
|
||||
pathex = [],
|
||||
|
||||
@@ -37,9 +37,9 @@ class WarningFindSpec(abc.MetaPathFinder):
|
||||
first."""
|
||||
if fullname.startswith("volatility3.framework.plugins."):
|
||||
warning = "Please do not use the volatility3.framework.plugins namespace directly, only use volatility3.plugins"
|
||||
# Pyinstaller uses walk_packages to import, but needs to read the modules to figure out dependencies
|
||||
# As such, we only print the warning when directly imported rather than from within walk_packages
|
||||
if inspect.stack()[-2].function != 'walk_packages':
|
||||
# Pyinstaller uses walk_packages/_collect_submodules to import, but needs to read the modules to figure out dependencies
|
||||
# As such, we only print the warning when directly imported rather than from within walk_packages/_collect_submodules
|
||||
if inspect.stack()[-2].function in ['walk_packages', '_collect_submodules']:
|
||||
raise Warning(warning)
|
||||
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ class CommandLine:
|
||||
parser.error(f"Cannot write configuration: file {args.save_config} already exists")
|
||||
with open(args.save_config, "w") as f:
|
||||
json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2)
|
||||
f.write("\n")
|
||||
except exceptions.UnsatisfiedException as excp:
|
||||
self.process_unsatisfied_exceptions(excp)
|
||||
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
|
||||
|
||||
@@ -246,6 +246,7 @@ class VolShell(cli.CommandLine):
|
||||
parser.error(f"Cannot write configuration: file {args.save_config} already exists")
|
||||
with open(args.save_config, "w") as f:
|
||||
json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2)
|
||||
f.write("\n")
|
||||
except exceptions.UnsatisfiedException as excp:
|
||||
self.process_unsatisfied_exceptions(excp)
|
||||
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
|
||||
@@ -256,7 +257,6 @@ class VolShell(cli.CommandLine):
|
||||
constructed.run()
|
||||
except exceptions.VolatilityException as excp:
|
||||
self.process_exceptions(excp)
|
||||
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -321,7 +321,7 @@ class SizedModule(Module):
|
||||
|
||||
The mapping should be sorted and should be quicker than reading
|
||||
the data We turn it into JSON to make a common string and use a
|
||||
quick hash, because collissions are unlikely
|
||||
quick hash, because collisions are unlikely
|
||||
"""
|
||||
layer = self._context.layers[self.layer_name]
|
||||
if not isinstance(layer, interfaces.layers.TranslationLayerInterface):
|
||||
|
||||
@@ -523,7 +523,7 @@ class ConstructableRequirementInterface(RequirementInterface):
|
||||
must happen after the class configuration value has been provided).
|
||||
These values are then provided to the object's constructor by name
|
||||
as arguments (as well as the standard `context` and `config_path`
|
||||
arguments.
|
||||
arguments).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
|
||||
@@ -307,7 +307,7 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
|
||||
while length > 0:
|
||||
chunk_size = min(length, scanner.chunk_size + scanner.overlap)
|
||||
yield [(layer_name, mapped_offset, chunk_size)], offset + chunk_size
|
||||
# It we've got more than the scanner's chunk_size, only move up by the chunk_size
|
||||
# If we've got more than the scanner's chunk_size, only move up by the chunk_size
|
||||
if chunk_size > scanner.chunk_size:
|
||||
chunk_size -= scanner.overlap
|
||||
length -= chunk_size
|
||||
@@ -517,7 +517,7 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
|
||||
yield output, chunk_position
|
||||
output = []
|
||||
chunk_position = chunk_start
|
||||
# Take from chunk_position as far as far as the block can go,
|
||||
# Take from chunk_position as far as the block can go,
|
||||
# or as much left of a scanner chunk as we can
|
||||
chunk_size = min(block_end - chunk_position,
|
||||
scanner.chunk_size + scanner.overlap - (chunk_position - chunk_start))
|
||||
|
||||
@@ -169,7 +169,7 @@ class BaseSymbolTableInterface:
|
||||
|
||||
def optional_set_type_class(self, name: str, clazz: Type[objects.ObjectInterface]) -> bool:
|
||||
"""Calls the set_type_class function but does not throw an exception.
|
||||
Returns whether setting the type class was successfull.
|
||||
Returns whether setting the type class was successful.
|
||||
Args:
|
||||
name: The name of the type to override the class for
|
||||
clazz: The actual class to override for the provided type name
|
||||
|
||||
@@ -88,6 +88,7 @@ class FileLayer(interfaces.layers.DataLayerInterface):
|
||||
self._accessor = resources.ResourceAccessor()
|
||||
self._file_: Optional[IO[Any]] = None
|
||||
self._size: Optional[int] = None
|
||||
self._maximum_address: Optional[int] = None
|
||||
# Construct the lock now (shared if made before threading) in case we ever need it
|
||||
self._lock: Union[DummyLock, threading.Lock] = DummyLock()
|
||||
if constants.PARALLELISM == constants.Parallelism.Threading:
|
||||
@@ -113,14 +114,15 @@ class FileLayer(interfaces.layers.DataLayerInterface):
|
||||
def maximum_address(self) -> int:
|
||||
"""Returns the largest available address in the space."""
|
||||
# Zero based, so we return the size of the file minus 1
|
||||
if self._size:
|
||||
return self._size
|
||||
if self._maximum_address:
|
||||
return self._maximum_address
|
||||
with self._lock:
|
||||
orig = self._file.tell()
|
||||
self._file.seek(0, 2)
|
||||
self._size = self._file.tell()
|
||||
self._file.seek(orig)
|
||||
return self._size
|
||||
self._maximum_address = self._size - 1
|
||||
return self._maximum_address
|
||||
|
||||
@property
|
||||
def minimum_address(self) -> int:
|
||||
@@ -189,7 +191,7 @@ class FileLayer(interfaces.layers.DataLayerInterface):
|
||||
"""Closes the file handle."""
|
||||
self._file.close()
|
||||
|
||||
def __del__(self) -> None:
|
||||
def __exit__(self) -> None:
|
||||
self.destroy()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
#
|
||||
import functools
|
||||
import json
|
||||
from typing import Optional, Dict, Any, Tuple, List, Set
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from volatility3.framework import interfaces, exceptions, constants
|
||||
from volatility3.framework.layers import segmented
|
||||
from volatility3.framework import constants, exceptions, interfaces
|
||||
from volatility3.framework.layers import scanners, segmented
|
||||
from volatility3.framework.symbols import intermed
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
"""A Qemu suspend-to-disk translation layer."""
|
||||
@@ -32,6 +37,34 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
SEGMENT_FLAG_XBZRLE = 0x40
|
||||
SEGMENT_FLAG_HOOK = 0x80
|
||||
|
||||
# See https://qemu.readthedocs.io/en/latest/devel/memory.html for more info
|
||||
#
|
||||
# At least the following values could occur for devices using > 3-4 GB RAM:
|
||||
# +--------------------------------+--------------------------------+------------+-------------+
|
||||
# | Architecture | Reference Code | Hole Start | Hole End |
|
||||
# +--------------------------------+--------------------------------+------------+-------------+
|
||||
# | PC i440FX + PIIX "New Default" | qemu/hw/i386/pc_piix.c:98 | 0xc0000000 | 0x100000000 |
|
||||
# | PC i440FX + PIIX "Old Default" | qemu/hw/i386/pc_piix.c:98 | 0xe0000000 | 0x100000000 |
|
||||
# | PC Q35 + ICH9 | qemu/hw/i386/pc_q35.c:141 | 0x80000000 | 0x100000000 |
|
||||
# | MicroVM | qemu/hw/i386/microvm.c:291 | 0xc0000000 | 0x100000000 |
|
||||
# | Xen | qemu/hw/i386/xen/xen-hvm.c:248 | 0xf0000000 | 0x100000000 |
|
||||
# +--------------------------------+--------------------------------+------------+-------------+
|
||||
#
|
||||
# For now, we assume that the parameter max-ram-below-4g is not set, since this parameter influences the size
|
||||
# and location of the memory gap. Deviating hole sizes could eventually be detected for Linux by e.g. scanning
|
||||
# for dmesg entries with a regex like rb'\[mem (0x[0-9a-f]{4,10})-0x[0-9a-f]{4,10}\] available for PCI devices'
|
||||
|
||||
distro_re = r"(\w+[\d{1,2}\.]*)"
|
||||
|
||||
pci_hole_table = {re.compile(r"^pc-i440fx-([23456789]|\d\d+)\.\d$"): (0xe0000000, 0xc0000000, 0x100000000),
|
||||
re.compile(r"^pc-i440fx-[01]\.\d$"): (0xe0000000, 0xe0000000, 0x100000000),
|
||||
re.compile(r"^pc-q35-\d\.\d$"): (0xb0000000, 0x80000000, 0x100000000),
|
||||
re.compile(r"^microvm$"): (0xc0000000, 0xc0000000, 0x100000000),
|
||||
re.compile(r"^xen$"): (0xf0000000, 0xf0000000, 0x100000000),
|
||||
re.compile(r"^pc-i440fx-" + distro_re + r"$"): (0xe0000000, 0xc0000000, 0x100000000),
|
||||
re.compile(r"^pc-q35-" + distro_re + r"$"): (0xb0000000, 0x80000000, 0x100000000),
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
context: interfaces.context.ContextInterface,
|
||||
config_path: str,
|
||||
@@ -39,8 +72,12 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
self._qemu_table_name = intermed.IntermediateSymbolTable.create(context, config_path, 'generic', 'qemu')
|
||||
self._configuration = None
|
||||
self._architecture = None
|
||||
self._compressed: Set[int] = set()
|
||||
self._current_segment_name = b''
|
||||
self._pci_hole_start = 0
|
||||
self._pci_hole_end = 0
|
||||
self._pci_hole_minimum = 0
|
||||
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
|
||||
|
||||
@classmethod
|
||||
@@ -50,6 +87,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
raise exceptions.LayerException(name, 'No QEMU magic bytes')
|
||||
if header[4:] != b'\x00\x00\x00\x03':
|
||||
raise exceptions.LayerException(name, 'Unsupported QEMU version found')
|
||||
vollog.debug("QEVM header found")
|
||||
|
||||
def _read_configuration(self, base_layer: interfaces.layers.DataLayerInterface, name: str) -> Any:
|
||||
"""Reads the JSON configuration from the end of the file"""
|
||||
@@ -73,12 +111,13 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
done = None
|
||||
segments = []
|
||||
|
||||
size_array = {}
|
||||
base_layer = self.context.layers[self._base_layer]
|
||||
|
||||
while not done:
|
||||
addr = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long long',
|
||||
offset = index,
|
||||
layer_name = self._base_layer)
|
||||
# Use struct.unpack here for performance improvements
|
||||
addr = struct.unpack('>Q', base_layer.read(index, 8))[0]
|
||||
|
||||
# Flags are stored in the n least significant bits, where n equals the bit-length of pagesize
|
||||
flags = addr & (page_size - 1)
|
||||
# addr equals the highest multiple of pagesize <= offset
|
||||
@@ -86,19 +125,29 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
addr = addr ^ (addr & (page_size - 1))
|
||||
index += 8
|
||||
|
||||
if addr >= self._pci_hole_start:
|
||||
addr += self._pci_hole_end - self._pci_hole_start
|
||||
|
||||
if flags & self.SEGMENT_FLAG_MEM_SIZE:
|
||||
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
|
||||
offset = index,
|
||||
layer_name = self._base_layer)
|
||||
while namelen != 0:
|
||||
# if base_layer.read(index + 1, namelen) == b'pc.ram':
|
||||
# total_size = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned long long',
|
||||
# offset = index + 1 + namelen,
|
||||
# layer_name = self._base_layer)
|
||||
total_size = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned long long',
|
||||
offset = index + 1 + namelen,
|
||||
layer_name = self._base_layer)
|
||||
size_array[base_layer.read(index + 1, namelen)] = total_size
|
||||
index += 1 + namelen + 8
|
||||
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
|
||||
offset = index,
|
||||
layer_name = self._base_layer)
|
||||
highest_possible_maximum = max([x[0] for x in self.pci_hole_table.values()]) + 1
|
||||
if size_array.get(b'pc.ram', highest_possible_maximum) < self._pci_hole_minimum:
|
||||
# Turns off the pci_hole if it's not supposed to be there
|
||||
vollog.debug(
|
||||
f"QEVM turning off PCI hole due to small image size: 0x{size_array.get(b'pc.ram'):x} < 0x{self._pci_hole_minimum:x}")
|
||||
self._pci_hole_start, self._pci_hole_end = 0, 0
|
||||
|
||||
if flags & (self.SEGMENT_FLAG_COMPRESS | self.SEGMENT_FLAG_PAGE):
|
||||
if not (flags & self.SEGMENT_FLAG_CONTINUE):
|
||||
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
|
||||
@@ -130,7 +179,26 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
index = 8
|
||||
section_info = dict()
|
||||
current_section_id = -1
|
||||
arch_detected = False
|
||||
while section_byte != self.QEVM_EOF and index <= base_layer.maximum_address:
|
||||
if index > 20 and not arch_detected:
|
||||
# We're past where the QEVM_CONFIGURATION might be, so set the values
|
||||
# If no architecture has been set, try to determine it using fallback mechanisms
|
||||
if not self._architecture:
|
||||
self._architecture = self._fallback_determine_architecture()
|
||||
if self._architecture is None:
|
||||
vollog.log(constants.LOGLEVEL_VV, f"QEVM architecture could not be determined")
|
||||
|
||||
# Once all segments have been read, determine the PCI hole if any
|
||||
for regex in self.pci_hole_table:
|
||||
if regex.match(self._architecture):
|
||||
self._pci_hole_minimum, self._pci_hole_start, self._pci_hole_end = self.pci_hole_table[regex]
|
||||
vollog.log(constants.LOGLEVEL_VVVV, f"QEVM architecture detected as: {self._architecture}")
|
||||
break
|
||||
else:
|
||||
vollog.log(constants.LOGLEVEL_VVVV, f"QEVM unknown architecture found: {self._architecture}")
|
||||
arch_detected = True
|
||||
|
||||
section_byte = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
|
||||
offset = index,
|
||||
layer_name = self._base_layer)
|
||||
@@ -139,6 +207,9 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
section_len = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
|
||||
offset = index,
|
||||
layer_name = self._base_layer)
|
||||
self._architecture = self.context.object(self._qemu_table_name + constants.BANG + 'string',
|
||||
offset = index + 4, layer_name = self._base_layer,
|
||||
max_length = section_len)
|
||||
index += 4 + section_len
|
||||
elif section_byte == self.QEVM_SECTION_START or section_byte == self.QEVM_SECTION_FULL:
|
||||
section_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
|
||||
@@ -189,6 +260,47 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
|
||||
else:
|
||||
raise exceptions.LayerException(self._name, f'QEMU unknown section encountered: {section_byte}')
|
||||
|
||||
def _fallback_determine_architecture(self) -> str:
|
||||
architecture_pattern = rb'pc-(i440fx|q35)-(\d{1,2}\.\d{1,2}|\w+[\d{1,2}\.]*)'
|
||||
default_suffix = "-2.0"
|
||||
base_layer = self.context.layers[self._base_layer]
|
||||
|
||||
vollog.log(constants.LOGLEVEL_VVVV, "QEVM fallback architecture detection used")
|
||||
|
||||
res = scanners.RegExScanner(architecture_pattern)
|
||||
for offset in base_layer.scan(context = self.context, scanner = res):
|
||||
line = base_layer.read(offset, 64)
|
||||
regex_results = re.search(architecture_pattern, line)
|
||||
architecture = regex_results.group().decode()
|
||||
return architecture
|
||||
|
||||
# If that does not work, look in configuration JSON for devices specific to a certain architecture
|
||||
architecture = None
|
||||
for device in self._configuration.get('devices', []):
|
||||
device_name = device.get('vmsd_name', '').lower()
|
||||
if 'i440fx' in device_name or 'piix' in device_name:
|
||||
architecture = 'pc-i440fx' + default_suffix
|
||||
break
|
||||
elif 'ich9' in device_name:
|
||||
architecture = 'pc-q35' + default_suffix
|
||||
break
|
||||
if architecture:
|
||||
vollog.log(constants.LOGLEVEL_VVV, f'Architecture version unknown, default used: {default_suffix}')
|
||||
return architecture
|
||||
|
||||
# Still haven't found architecture, switch to fallback-method
|
||||
architecture_pattern = rb'Standard PC \((i440FX|Q35)'
|
||||
res = scanners.RegExScanner(architecture_pattern)
|
||||
for offset in base_layer.scan(context = self.context, scanner = res):
|
||||
line = base_layer.read(offset, 64)
|
||||
regex_results = re.search(architecture_pattern, line)
|
||||
architecture = "pc-" + regex_results.groups()[0].decode().lower() + default_suffix
|
||||
vollog.log(constants.LOGLEVEL_VVV, f'Architecture version unknown, default used: {default_suffix}')
|
||||
return architecture
|
||||
|
||||
vollog.warning("Could not determine QEMU target architecture!")
|
||||
return None
|
||||
|
||||
def extract_data(self, index, name, version_id):
|
||||
if name == 'ram':
|
||||
if version_id != 4:
|
||||
|
||||
@@ -171,6 +171,8 @@ class ResourceAccessor(object):
|
||||
cache_file.write(block)
|
||||
block = fp.read(block_size)
|
||||
cache_file.close()
|
||||
else:
|
||||
vollog.debug(f"Using already cached file at: {temp_filename}")
|
||||
# Re-open the cache with a different mode
|
||||
# Since we don't want people thinking they're able to save to the cache file,
|
||||
# open it in read mode only and allow breakages to happen if they wanted to write
|
||||
|
||||
@@ -63,7 +63,7 @@ class ObjectTemplate(interfaces.objects.Template):
|
||||
object_info: interfaces.objects.ObjectInformation) -> interfaces.objects.ObjectInterface:
|
||||
"""Constructs the object.
|
||||
|
||||
Returns: an object adhereing to the :class:`~volatility3.framework.interfaces.objects.ObjectInterface`
|
||||
Returns: an object adhering to the :class:`~volatility3.framework.interfaces.objects.ObjectInterface`
|
||||
"""
|
||||
arguments: Dict[str, Any] = {}
|
||||
for arg in self.vol:
|
||||
|
||||
@@ -80,7 +80,7 @@ class Check_syscall(plugins.PluginInterface):
|
||||
|
||||
def _get_table_info_disassembly(self, ptr_sz, vmlinux):
|
||||
"""Find the size of the system call table by disassembling functions
|
||||
that immediately reference it in their first isntruction This is in the
|
||||
that immediately reference it in their first instruction This is in the
|
||||
form 'cmp reg,NR_syscalls'."""
|
||||
table_size = 0
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
# Author: Gustavo Moreira
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# This file is Copyright 2022 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 Optional
|
||||
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework import symbols, exceptions, renderers, interfaces
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.plugins.linux import pslist
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
class PsAux(plugins.PluginInterface):
|
||||
""" Lists processes with their command line arguments """
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
# Since we're calling the plugin, make sure we have the plugin's requirements
|
||||
return [
|
||||
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
|
||||
requirements.ListRequirement(name = 'pid',
|
||||
description = 'Filter on specific process IDs',
|
||||
element_type = int,
|
||||
optional = True)
|
||||
]
|
||||
|
||||
def _get_command_line_args(self, task: interfaces.objects.ObjectInterface,
|
||||
name: str) -> Optional[str]:
|
||||
"""
|
||||
Reads the command line arguments of a process
|
||||
These are stored on the userland stack
|
||||
Kernel threads re-use the process data structure, but do not have a valid 'mm' pointer
|
||||
|
||||
Parameters:
|
||||
task: task_struct object of the process
|
||||
name: string name of the process (from task.comm)
|
||||
"""
|
||||
|
||||
# kernel threads never have an mm as they do not have userland mappings
|
||||
try:
|
||||
mm = task.mm
|
||||
except exceptions.InvalidAddressException:
|
||||
mm = None
|
||||
|
||||
if mm:
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if proc_layer_name is None:
|
||||
return renderers.UnreadableValue()
|
||||
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
# read argv from userland
|
||||
start = task.mm.arg_start
|
||||
|
||||
# get the size of the arguments with sanity checking
|
||||
size_to_read = task.mm.arg_end - task.mm.arg_start
|
||||
if not (0 < size_to_read <= 4096):
|
||||
return renderers.UnreadableValue()
|
||||
|
||||
# attempt to read it all as partial values are invalid and misleading
|
||||
try:
|
||||
argv = proc_layer.read(start, size_to_read)
|
||||
except exceptions.InvalidAddressException:
|
||||
return renderers.UnreadableValue()
|
||||
|
||||
# the arguments are null byte terminated, replace the nulls with spaces
|
||||
s = argv.decode().split('\x00')
|
||||
args = " ".join(s)
|
||||
else:
|
||||
# kernel thread
|
||||
# [ ] mimics ps on a live system
|
||||
# also helps identify malware masquerading as a kernel thread, which is fairly common
|
||||
args = "[" + name + "]"
|
||||
|
||||
# remove trailing space, if present
|
||||
if len(args) > 1 and args[-1] == " ":
|
||||
args = args[:-1]
|
||||
|
||||
return args
|
||||
|
||||
def _generator(self, tasks):
|
||||
""" Generates a listing of processes along with command line arguments """
|
||||
|
||||
# walk the process list and report the arguments
|
||||
for task in tasks:
|
||||
pid = task.pid
|
||||
|
||||
try:
|
||||
ppid = task.parent.pid
|
||||
except exceptions.InvalidAddressException:
|
||||
ppid = 0
|
||||
|
||||
name = utility.array_to_string(task.comm)
|
||||
|
||||
args = self._get_command_line_args(task, name)
|
||||
|
||||
yield (0, (pid, ppid, name, args))
|
||||
|
||||
def run(self):
|
||||
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
|
||||
|
||||
return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str), ("ARGS", str)],
|
||||
self._generator(
|
||||
pslist.PsList.list_tasks(self.context,
|
||||
self.config['kernel'],
|
||||
filter_func = filter_func)))
|
||||
|
||||
@@ -19,7 +19,7 @@ class PsTree(pslist.PsList):
|
||||
"""Finds how deep the PID is in the tasks hierarchy.
|
||||
|
||||
Args:
|
||||
pid: PID to find the level in the hierachy
|
||||
pid: PID to find the level in the hierarchy
|
||||
"""
|
||||
seen = set([pid])
|
||||
level = 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# This file is opyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class Kevents(interfaces.plugins.PluginInterface):
|
||||
@classmethod
|
||||
def _walk_klist_array(cls, kernel, fdp, array_pointer_member, array_size_member):
|
||||
"""
|
||||
Convience wrapper for walking an array of lists of kernel events
|
||||
Convenience wrapper for walking an array of lists of kernel events
|
||||
Handles invalid address references
|
||||
"""
|
||||
try:
|
||||
|
||||
@@ -101,7 +101,7 @@ class Timeliner(interfaces.plugins.PluginInterface):
|
||||
|
||||
return [sortable(timestamp) for timestamp in data[2:]]
|
||||
|
||||
def _generator(self, runable_plugins: List[TimeLinerInterface]) -> Optional[Iterable[Tuple[int, Tuple]]]:
|
||||
def _generator(self, runnable_plugins: List[TimeLinerInterface]) -> Optional[Iterable[Tuple[int, Tuple]]]:
|
||||
"""Takes a timeline, sorts it and output the data from each relevant
|
||||
row from each plugin."""
|
||||
# Generate the results for each plugin
|
||||
@@ -115,9 +115,9 @@ class Timeliner(interfaces.plugins.PluginInterface):
|
||||
file_data = None
|
||||
fp = None
|
||||
|
||||
for plugin in runable_plugins:
|
||||
for plugin in runnable_plugins:
|
||||
plugin_name = plugin.__class__.__name__
|
||||
self._progress_callback((runable_plugins.index(plugin) * 100) // len(runable_plugins),
|
||||
self._progress_callback((runnable_plugins.index(plugin) * 100) // len(runnable_plugins),
|
||||
f"Running plugin {plugin_name}...")
|
||||
try:
|
||||
vollog.log(logging.INFO, f"Running {plugin_name}")
|
||||
|
||||
@@ -25,7 +25,7 @@ class ModScan(interfaces.plugins.PluginInterface):
|
||||
return [
|
||||
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
requirements.VersionRequirement(name = 'poolerscanner',
|
||||
requirements.VersionRequirement(name = 'poolscanner',
|
||||
component = poolscanner.PoolScanner,
|
||||
version = (1, 0, 0)),
|
||||
requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# For a thorough walkthrough on how the R&D was performed to develop this plugin,
|
||||
# please see our blogpost here:
|
||||
#
|
||||
# <insert blog URL once published>
|
||||
# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
@@ -95,10 +95,10 @@ class SSDT(plugins.PluginInterface):
|
||||
if is_kernel_64:
|
||||
array_subtype = "long"
|
||||
|
||||
def kvo_calulator(func: int) -> int:
|
||||
def kvo_calculator(func: int) -> int:
|
||||
return kvo + service_table_address + (func >> 4)
|
||||
|
||||
find_address = kvo_calulator
|
||||
find_address = kvo_calculator
|
||||
else:
|
||||
array_subtype = "unsigned long"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 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, Tuple, Iterator
|
||||
from typing import Iterator, List, Tuple
|
||||
|
||||
from volatility3 import framework
|
||||
from volatility3.framework import exceptions, constants, interfaces, objects
|
||||
from volatility3.framework import constants, exceptions, interfaces, objects
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.framework.symbols import intermed
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
@@ -29,7 +29,9 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
self.set_type_class('files_struct', extensions.files_struct)
|
||||
self.set_type_class('vfsmount', extensions.vfsmount)
|
||||
self.set_type_class('kobject', extensions.kobject)
|
||||
self.set_type_class('mnt_namespace', extensions.mnt_namespace)
|
||||
|
||||
if 'mnt_namespace' in self.types:
|
||||
self.set_type_class('mnt_namespace', extensions.mnt_namespace)
|
||||
|
||||
if 'module' in self.types:
|
||||
self.set_type_class('module', extensions.module)
|
||||
@@ -267,4 +269,4 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
while list_start:
|
||||
list_struct = vmlinux.object(object_type = struct_name, offset = list_start.vol.offset)
|
||||
yield list_struct
|
||||
list_start = getattr(list_struct, list_member)
|
||||
list_start = getattr(list_struct, list_member)
|
||||
|
||||
@@ -38,4 +38,4 @@ class WindowsMetadata(interfaces.symbols.MetadataInterface):
|
||||
|
||||
|
||||
class LinuxMetadata(interfaces.symbols.MetadataInterface):
|
||||
"""Class to handle the etadata from a Linux symbol table."""
|
||||
"""Class to handle the metadata from a Linux symbol table."""
|
||||
|
||||
@@ -746,7 +746,10 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
|
||||
trans_layer = self._context.layers[layer]
|
||||
|
||||
try:
|
||||
trans_layer.is_valid(self.vol.offset)
|
||||
is_valid = trans_layer.is_valid(self.vol.offset)
|
||||
if not is_valid:
|
||||
return
|
||||
|
||||
link = getattr(self, direction).dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
@@ -761,9 +764,7 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
|
||||
while link.vol.offset not in seen:
|
||||
obj_offset = link.vol.offset - relative_offset
|
||||
|
||||
try:
|
||||
trans_layer.is_valid(obj_offset)
|
||||
except exceptions.InvalidAddressException:
|
||||
if not trans_layer.is_valid(obj_offset):
|
||||
return
|
||||
|
||||
obj = self._context.object(symbol_type,
|
||||
|
||||
@@ -10,10 +10,10 @@ import os
|
||||
import re
|
||||
import struct
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
from urllib import request, parse
|
||||
from urllib import parse, request
|
||||
|
||||
from volatility3 import symbols
|
||||
from volatility3.framework import constants, interfaces, exceptions
|
||||
from volatility3.framework import constants, contexts, exceptions, interfaces
|
||||
from volatility3.framework.configuration.requirements import SymbolTableRequirement
|
||||
from volatility3.framework.symbols import intermed
|
||||
from volatility3.framework.symbols.windows import pdbconv
|
||||
@@ -24,7 +24,7 @@ vollog = logging.getLogger(__name__)
|
||||
class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
"""Class to handle and manage all getting symbols based on MZ header"""
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 1)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
@@ -131,14 +131,14 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
# Check it is actually the MZ header
|
||||
if mz_sig != b"MZ":
|
||||
return None
|
||||
|
||||
|
||||
nt_header_start, = struct.unpack("<I", layer.read(offset + 0x3C, 4))
|
||||
pe_sig = layer.read(offset + nt_header_start, 2)
|
||||
|
||||
|
||||
# Check it is actually the Nt Headers
|
||||
if pe_sig != b"PE":
|
||||
return None
|
||||
|
||||
|
||||
optional_header_size, = struct.unpack('<H', layer.read(offset + nt_header_start + 0x14, 2))
|
||||
# Just enough to tell us the max size
|
||||
pe_header = layer.read(offset, nt_header_start + 0x16 + optional_header_size)
|
||||
@@ -146,7 +146,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
max_size = pe_data.OPTIONAL_HEADER.SizeOfImage
|
||||
|
||||
# Proper data
|
||||
virtual_data = layer.read(offset, max_size, pad=True)
|
||||
virtual_data = layer.read(offset, max_size, pad = True)
|
||||
pe_data = pefile.PE(data = virtual_data)
|
||||
|
||||
# De-virtualize the memory
|
||||
@@ -291,7 +291,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
|
||||
@classmethod
|
||||
def symbol_table_from_pdb(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str,
|
||||
pdb_name: str, module_offset: int, module_size: int) -> str:
|
||||
pdb_name: str, module_offset: int = None, module_size: int = None) -> str:
|
||||
"""Creates symbol table for a module in the specified layer_name.
|
||||
|
||||
Searches the memory section of the loaded module for its PDB GUID
|
||||
@@ -307,6 +307,19 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
Returns:
|
||||
The name of the constructed and loaded symbol table
|
||||
"""
|
||||
_, symbol_table_name = cls._modtable_from_pdb(context, config_path, layer_name, pdb_name, module_offset,
|
||||
module_size)
|
||||
return symbol_table_name
|
||||
|
||||
@classmethod
|
||||
def _modtable_from_pdb(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str,
|
||||
pdb_name: str, module_offset: int = None, module_size: int = None,
|
||||
create_module: bool = False) -> Tuple[Optional[str], Optional[str]]:
|
||||
|
||||
if module_offset is None:
|
||||
module_offset = context.layers[layer_name].minimum_address
|
||||
if module_size is None:
|
||||
module_size = context.layers[layer_name].maximum_address - module_offset
|
||||
|
||||
guids = list(
|
||||
cls.pdbname_scan(context,
|
||||
@@ -323,12 +336,46 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
|
||||
vollog.debug(f"Found {guid['pdb_name']}: {guid['GUID']}-{guid['age']}")
|
||||
|
||||
return cls.load_windows_symbol_table(context,
|
||||
guid["GUID"],
|
||||
guid["age"],
|
||||
guid["pdb_name"],
|
||||
"volatility3.framework.symbols.intermed.IntermediateSymbolTable",
|
||||
config_path = config_path)
|
||||
module_name = guid["pdb_name"].strip('.pdb')
|
||||
|
||||
symbol_table_name = cls.load_windows_symbol_table(context,
|
||||
guid["GUID"],
|
||||
guid["age"],
|
||||
guid["pdb_name"],
|
||||
"volatility3.framework.symbols.intermed.IntermediateSymbolTable",
|
||||
config_path = config_path)
|
||||
|
||||
new_module_name = None
|
||||
if create_module:
|
||||
new_module = contexts.Module.create(context, module_name, layer_name, offset = guid['mz_offset'],
|
||||
symbol_table_name = symbol_table_name)
|
||||
new_module_name = new_module.name
|
||||
|
||||
return new_module_name, symbol_table_name
|
||||
|
||||
@classmethod
|
||||
def module_from_pdb(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str,
|
||||
pdb_name: str, module_offset: int = None, module_size: int = None) -> str:
|
||||
"""Creates a module in the specified layer_name based on a pdb name.
|
||||
|
||||
Searches the memory section of the loaded module for its PDB GUID
|
||||
and loads the associated symbol table into the symbol space.
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
config_path: The config path where to find symbol files
|
||||
layer_name: The name of the layer on which to operate
|
||||
module_offset: This memory dump's module image offset
|
||||
module_size: The size of the module for this dump
|
||||
|
||||
Returns:
|
||||
The name of the constructed and loaded symbol table
|
||||
"""
|
||||
|
||||
module_name, _ = cls._modtable_from_pdb(context, config_path, layer_name, pdb_name, module_offset,
|
||||
module_size, create_module = True)
|
||||
|
||||
return module_name
|
||||
|
||||
|
||||
class PdbSignatureScanner(interfaces.layers.ScannerInterface):
|
||||
|
||||
Reference in New Issue
Block a user