mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-07 18:27:39 +02:00
Merge branch 'develop' into issue_985
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
name: Install Volatility3 test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
install_test:
|
||||
runs-on: ${{ matrix.host }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
host: [ ubuntu-latest, windows-latest ]
|
||||
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup python-pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install volatility3
|
||||
run: pip install .
|
||||
|
||||
- name: Run volatility3
|
||||
run: vol --help
|
||||
@@ -0,0 +1,37 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Volatility 3
|
||||
message: >-
|
||||
If you reference this software, please feel free to cite
|
||||
it using the information below.
|
||||
type: software
|
||||
authors:
|
||||
- name: Volatility Foundation
|
||||
country: US
|
||||
website: 'https://www.volatilityfoundation.org/'
|
||||
identifiers:
|
||||
- type: url
|
||||
value: 'https://github.com/volatilityfoundation/volatility3'
|
||||
description: Volatility 3 source code respository
|
||||
repository-code: 'https://github.com/volatilityfoundation/volatility3'
|
||||
url: 'https://github.com/volatilityfoundation/volatility3'
|
||||
abstract: >-
|
||||
Volatility is the world's most widely used framework for
|
||||
extracting digital artifacts from volatile memory (RAM)
|
||||
samples. The extraction techniques are performed
|
||||
completely independent of the system being investigated
|
||||
but offer visibility into the runtime state of the system.
|
||||
The framework is intended to introduce people to the
|
||||
techniques and complexities associated with extracting
|
||||
digital artifacts from volatile memory samples and provide
|
||||
a platform for further work into this exciting area of
|
||||
research.
|
||||
keywords:
|
||||
- malware
|
||||
- forensics
|
||||
- memory
|
||||
- python
|
||||
- ram
|
||||
- volatility
|
||||
+1
-1
@@ -27,7 +27,7 @@ def setup(app):
|
||||
|
||||
source_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
sphinx.ext.apidoc.main(
|
||||
argv=["-e", "-M", "-f", "-T", "-o", source_dir, volatility_directory]
|
||||
["-e", "-M", "-f", "-T", "-o", source_dir, volatility_directory]
|
||||
)
|
||||
|
||||
# Go through the volatility3.framework.plugins files and change them to volatility3.plugins
|
||||
|
||||
@@ -54,6 +54,12 @@ also be included, which can be found in `volatility3.constants.PLUGINS_PATH`.
|
||||
volatility3.plugins.__path__ = <new_plugin_path> + constants.PLUGINS_PATH
|
||||
failures = framework.import_files(volatility3.plugins, True)
|
||||
|
||||
.. note::
|
||||
|
||||
Volatility uses the `volatility3.plugins` namespace for all plugins (including those in `volatility3.framework.plugins`).
|
||||
Please ensure you only use `volatility3.plugins` and only ever import plugins from this namespace.
|
||||
This ensures the ability of users to override core plugins without needing write access to the framework directory.
|
||||
|
||||
Once the plugins have been imported, we can interrogate which plugins are available. The
|
||||
:py:func:`~volatility3.framework.list_plugins` call will
|
||||
return a dictionary of plugin names and the plugin classes.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# The following packages are required for core functionality.
|
||||
pefile>=2017.8.1
|
||||
pefile>=2023.2.7
|
||||
|
||||
# The following packages are optional.
|
||||
# If certain packages are not necessary, place a comment (#) at the start of the line.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# These packages are required for core functionality.
|
||||
pefile>=2017.8.1 #foo
|
||||
pefile>=2023.2.7 #foo
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
# The following packages are required for core functionality.
|
||||
pefile>=2017.8.1
|
||||
pefile>=2023.2.7
|
||||
|
||||
# The following packages are optional.
|
||||
# If certain packages are not necessary, place a comment (#) at the start of the line.
|
||||
@@ -16,3 +16,7 @@ pycryptodome
|
||||
|
||||
# This is required for memory acquisition via leechcore/pcileech.
|
||||
leechcorepyc>=2.4.0
|
||||
|
||||
# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage
|
||||
gcsfs>=2023.1.0
|
||||
s3fs>=2023.1.0
|
||||
@@ -12,7 +12,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
||||
|
||||
def get_install_requires():
|
||||
requirements = []
|
||||
with open("requirements-minimal.txt", "r", encoding = "utf-8") as fh:
|
||||
with open("requirements-minimal.txt", "r", encoding="utf-8") as fh:
|
||||
for line in fh.readlines():
|
||||
stripped_line = line.strip()
|
||||
if stripped_line == "" or stripped_line.startswith("#"):
|
||||
@@ -20,6 +20,7 @@ def get_install_requires():
|
||||
requirements.append(stripped_line)
|
||||
return requirements
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="volatility3",
|
||||
description="Memory forensics framework",
|
||||
@@ -36,12 +37,12 @@ setuptools.setup(
|
||||
"Documentation": "https://volatility3.readthedocs.io/",
|
||||
"Source Code": "https://github.com/volatilityfoundation/volatility3",
|
||||
},
|
||||
packages=setuptools.find_namespace_packages(
|
||||
include=["volatility3", "volatility3.*"]
|
||||
),
|
||||
package_dir={"volatility3": "volatility3"},
|
||||
python_requires=">=3.7.0",
|
||||
include_package_data=True,
|
||||
exclude_package_data={"": ["development", "development.*"], "development": ["*"]},
|
||||
packages=setuptools.find_namespace_packages(
|
||||
exclude=["development", "development.*"]
|
||||
),
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"vol = volatility3.cli:main",
|
||||
|
||||
@@ -662,7 +662,7 @@ class CommandLine:
|
||||
def close(self):
|
||||
# Don't overcommit
|
||||
if self.closed:
|
||||
return
|
||||
return None
|
||||
|
||||
self.seek(0)
|
||||
|
||||
@@ -712,7 +712,7 @@ class CommandLine:
|
||||
"""Closes and commits the file (by moving the temporary file to the correct name"""
|
||||
# Don't overcommit
|
||||
if self._file.closed:
|
||||
return
|
||||
return None
|
||||
|
||||
self._file.close()
|
||||
output_filename = self._get_final_filename()
|
||||
|
||||
@@ -108,7 +108,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
"""Describes the available commands"""
|
||||
if args:
|
||||
help(*args)
|
||||
return
|
||||
return None
|
||||
|
||||
variables = []
|
||||
print("\nMethods:")
|
||||
@@ -325,7 +325,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
(str, interfaces.objects.ObjectInterface, interfaces.objects.Template),
|
||||
):
|
||||
print("Cannot display information about non-type object")
|
||||
return
|
||||
return None
|
||||
|
||||
if not isinstance(object, str):
|
||||
# Mypy requires us to order things this way
|
||||
@@ -453,7 +453,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
"""Prints an alphabetical list of symbols for a symbol table"""
|
||||
if symbol_table is None:
|
||||
print("No symbol table provided")
|
||||
return
|
||||
return None
|
||||
longest_offset = longest_name = 0
|
||||
|
||||
table = self.context.symbol_space[symbol_table]
|
||||
|
||||
@@ -35,9 +35,9 @@ class Volshell(generic.Volshell):
|
||||
process_layer = task.add_process_layer()
|
||||
if process_layer is not None:
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"Layer for task ID {pid} could not be constructed")
|
||||
return
|
||||
return None
|
||||
print(f"No task with task ID {pid} found")
|
||||
|
||||
def list_tasks(self):
|
||||
|
||||
@@ -35,9 +35,9 @@ class Volshell(generic.Volshell):
|
||||
process_layer = task.add_process_layer()
|
||||
if process_layer is not None:
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"Layer for task ID {pid} could not be constructed")
|
||||
return
|
||||
return None
|
||||
print(f"No task with task ID {pid} found")
|
||||
|
||||
def list_tasks(self, method=None):
|
||||
|
||||
@@ -32,7 +32,7 @@ class Volshell(generic.Volshell):
|
||||
if process.UniqueProcessId == pid:
|
||||
process_layer = process.add_process_layer()
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"No process with process ID {pid} found")
|
||||
|
||||
def list_processes(self):
|
||||
|
||||
@@ -29,9 +29,9 @@ class KernelModule(interfaces.automagic.AutomagicInterface):
|
||||
requirement.requirements[req],
|
||||
progress_callback,
|
||||
)
|
||||
return
|
||||
return None
|
||||
if not requirement.unsatisfied(context, config_path):
|
||||
return
|
||||
return None
|
||||
# The requirement is unfulfilled and is a ModuleRequirement
|
||||
|
||||
context.config[
|
||||
@@ -43,7 +43,7 @@ class KernelModule(interfaces.automagic.AutomagicInterface):
|
||||
requirement.requirements[req].unsatisfied(context, new_config_path)
|
||||
and req != "offset"
|
||||
):
|
||||
return
|
||||
return None
|
||||
|
||||
# We now just have the offset requirement, but the layer requirement has been fulfilled.
|
||||
# Unfortunately we don't know the layer name requirement's exact name
|
||||
|
||||
@@ -103,7 +103,7 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
|
||||
appropriate_config_path, layer_name = result
|
||||
context.config.merge(appropriate_config_path, subconfig)
|
||||
context.config[appropriate_config_path] = top_layer_name
|
||||
return
|
||||
return None
|
||||
self._cached = None
|
||||
|
||||
new_context = context.clone()
|
||||
@@ -156,6 +156,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
|
||||
self._cached = context.config.get(path, None), context.config.branch(
|
||||
path
|
||||
)
|
||||
vollog.debug(
|
||||
f"physical_layer maximum_address: {physical_layer.maximum_address}"
|
||||
)
|
||||
vollog.debug(f"Stacked layers: {stacked_layers}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -429,7 +429,7 @@ class SqliteCache(CacheManagerInterface):
|
||||
progress_callback(0, "Reading remote ISF list")
|
||||
cursor = self._database.cursor()
|
||||
cursor.execute(
|
||||
f"SELECT cached FROM cache WHERE local = 0 and cached < datetime('now', {self.cache_period})"
|
||||
f"SELECT cached FROM cache WHERE local = 0 and cached < datetime('now', '{self.cache_period}')"
|
||||
)
|
||||
remote_identifiers = RemoteIdentifierFormat(constants.REMOTE_ISF_URL)
|
||||
progress_callback(50, "Reading remote ISF list")
|
||||
@@ -438,9 +438,13 @@ class SqliteCache(CacheManagerInterface):
|
||||
{}, operating_system=operating_system
|
||||
)
|
||||
for identifier, location in identifiers:
|
||||
identifier = identifier.rstrip()
|
||||
identifier = (
|
||||
identifier[:-1] if identifier.endswith(b"\x00") else identifier
|
||||
) # Linux banners dumped by dwarf2json end with "\x00\n". If not stripped, the banner cannot match.
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO cache(identifier, location, operating_system, local, cached) VALUES (?, ?, ?, ?, datetime('now'))",
|
||||
(location, identifier, operating_system, False),
|
||||
(identifier, location, operating_system, False),
|
||||
)
|
||||
progress_callback(100, "Reading remote ISF list")
|
||||
self._database.commit()
|
||||
|
||||
@@ -69,7 +69,7 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
|
||||
|
||||
# Bomb out early if our details haven't been configured
|
||||
if self.symbol_class is None:
|
||||
return
|
||||
return None
|
||||
|
||||
self._requirements = self.find_requirements(
|
||||
context,
|
||||
@@ -120,7 +120,7 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
|
||||
|
||||
# Bomb out early if there's no banners
|
||||
if not self.banners:
|
||||
return
|
||||
return None
|
||||
|
||||
mss = scanners.MultiStringScanner([x for x in self.banners if x is not None])
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ BANG = "!"
|
||||
# We use the SemVer 2.0.0 versioning scheme
|
||||
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
|
||||
VERSION_MINOR = 5 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 0 # Number of changes that do not change the interface
|
||||
VERSION_PATCH = 2 # Number of changes that do not change the interface
|
||||
VERSION_SUFFIX = ""
|
||||
|
||||
# TODO: At version 2.0.0, remove the symbol_shift feature
|
||||
|
||||
@@ -279,3 +279,5 @@ CAPABILITIES = (
|
||||
"bpf",
|
||||
"checkpoint_restore",
|
||||
)
|
||||
|
||||
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1
|
||||
|
||||
@@ -678,16 +678,12 @@ class LayerContainer(collections.abc.Mapping):
|
||||
name: The name of the layer to delete
|
||||
"""
|
||||
for layer in self._layers:
|
||||
depend_list = [
|
||||
superlayer
|
||||
for superlayer in self._layers
|
||||
if name in self._layers[layer].dependencies
|
||||
]
|
||||
if depend_list:
|
||||
if name in self._layers[layer].dependencies:
|
||||
raise exceptions.LayerException(
|
||||
self._layers[layer].name,
|
||||
f"Layer {self._layers[layer].name} is depended upon: {', '.join(depend_list)}",
|
||||
f"Layer {self._layers[layer].name} is depended upon by {layer}",
|
||||
)
|
||||
# Otherwise, wipe out the layer
|
||||
self._layers[name].destroy()
|
||||
del self._layers[name]
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class FileHandlerInterface(io.RawIOBase):
|
||||
return self._preferred_filename
|
||||
|
||||
@preferred_filename.setter
|
||||
def preferred_filename(self, filename):
|
||||
def preferred_filename(self, filename: str):
|
||||
"""Sets the preferred filename"""
|
||||
if self.closed:
|
||||
raise IOError("FileHandler name cannot be changed once closed")
|
||||
@@ -57,6 +57,18 @@ class FileHandlerInterface(io.RawIOBase):
|
||||
def close(self):
|
||||
"""Method that commits the file and fixes the final filename for use"""
|
||||
|
||||
@staticmethod
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
"""Sanititizes the filename to ensure only a specific whitelist of characters is allowed through"""
|
||||
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]\{\}!$%^:#~?<>,|"
|
||||
result = ""
|
||||
for char in filename:
|
||||
if char in allowed:
|
||||
result += char
|
||||
else:
|
||||
result += "?"
|
||||
return result
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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
|
||||
#
|
||||
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import Optional, Any, List
|
||||
|
||||
try:
|
||||
import s3fs
|
||||
|
||||
HAS_S3FS = True
|
||||
except ImportError:
|
||||
HAS_S3FS = False
|
||||
|
||||
try:
|
||||
import gcsfs
|
||||
|
||||
HAS_GCSFS = True
|
||||
except ImportError:
|
||||
HAS_GCSFS = False
|
||||
|
||||
from volatility3.framework import exceptions
|
||||
from volatility3.framework.layers import resources
|
||||
|
||||
vollog = logging.getLogger(__file__)
|
||||
|
||||
if HAS_S3FS:
|
||||
|
||||
class S3FileSystemHandler(resources.VolatilityHandler):
|
||||
@classmethod
|
||||
def non_cached_schemes(cls) -> List[str]:
|
||||
return ["s3"]
|
||||
|
||||
@staticmethod
|
||||
def default_open(req: urllib.request.Request) -> Optional[Any]:
|
||||
"""Handles the request if it's the s3 scheme."""
|
||||
if req.type == "s3":
|
||||
object_uri = "://".join(req.full_url.split("://")[1:])
|
||||
return s3fs.S3FileSystem().open(object_uri)
|
||||
return None
|
||||
|
||||
|
||||
if HAS_GCSFS:
|
||||
|
||||
class GSFileSystemHandler(resources.VolatilityHandler):
|
||||
@classmethod
|
||||
def non_cached_schemes(cls) -> List[str]:
|
||||
return ["gs"]
|
||||
|
||||
@staticmethod
|
||||
def default_open(req: urllib.request.Request) -> Optional[Any]:
|
||||
"""Handles the request if it's the gs scheme."""
|
||||
if req.type == "gs":
|
||||
object_uri = "://".join(req.full_url.split("://")[1:])
|
||||
return gcsfs.GCSFileSystem().open(object_uri)
|
||||
return None
|
||||
@@ -111,6 +111,11 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
"""Returns whether a particular page is valid based on its entry."""
|
||||
return bool(entry & 1)
|
||||
|
||||
@staticmethod
|
||||
def _page_is_dirty(entry: int) -> bool:
|
||||
"""Returns whether a particular page is dirty based on its entry."""
|
||||
return bool(entry & (1 << 6))
|
||||
|
||||
def canonicalize(self, addr: int) -> int:
|
||||
"""Canonicalizes an address by performing an appropiate sign extension on the higher addresses"""
|
||||
if self._bits_per_register <= self._maxvirtaddr:
|
||||
@@ -259,6 +264,10 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
except exceptions.InvalidAddressException:
|
||||
return False
|
||||
|
||||
def is_dirty(self, offset: int) -> bool:
|
||||
"""Returns whether the page at offset is marked dirty"""
|
||||
return self._page_is_dirty(self._translate_entry(offset)[0])
|
||||
|
||||
def mapping(
|
||||
self, offset: int, length: int, ignore_errors: bool = False
|
||||
) -> Iterable[Tuple[int, int, int, int, str]]:
|
||||
@@ -322,9 +331,9 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
except exceptions.InvalidAddressException:
|
||||
if not ignore_errors:
|
||||
raise
|
||||
return
|
||||
return None
|
||||
yield offset, length, mapped_offset, length, layer_name
|
||||
return
|
||||
return None
|
||||
while length > 0:
|
||||
try:
|
||||
chunk_offset, page_size, layer_name = self._translate(offset)
|
||||
|
||||
@@ -47,7 +47,7 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
|
||||
def read_streams(self):
|
||||
# Shortcut in case they've already been read
|
||||
if self._streams:
|
||||
return
|
||||
return None
|
||||
|
||||
# Recover the root table, by recovering the root table index table...
|
||||
module = self.context.module(self.pdb_symbol_table, self._base_layer, offset=0)
|
||||
|
||||
@@ -171,7 +171,15 @@ class RegistryHive(linear.LinearlyMappedLayer):
|
||||
node (default) or a list of nodes from root to the current node
|
||||
(if return_list is true).
|
||||
"""
|
||||
node_key = [self.get_node(self.root_cell_offset)]
|
||||
root_node = self.get_node(self.root_cell_offset)
|
||||
if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"):
|
||||
raise RegistryFormatException(
|
||||
self.name,
|
||||
"Encountered {} instead of _CM_KEY_NODE".format(
|
||||
root_node.vol.type_name
|
||||
),
|
||||
)
|
||||
node_key = [root_node]
|
||||
if key.endswith("\\"):
|
||||
key = key[:-1]
|
||||
key_array = key.split("\\")
|
||||
|
||||
@@ -126,9 +126,9 @@ class NonLinearlySegmentedLayer(
|
||||
current_offset = logical_offset
|
||||
# If it starts too late then we're done
|
||||
if logical_offset > offset + length:
|
||||
return
|
||||
return None
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
# Crop it to the amount we need left
|
||||
chunk_size = min(size, length + offset - logical_offset)
|
||||
yield logical_offset, chunk_size, mapped_offset, mapped_size, self._base_layer
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import struct
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from volatility3.framework import constants, exceptions, interfaces
|
||||
@@ -232,6 +233,11 @@ class VmwareStacker(interfaces.automagic.StackerLayerInterface):
|
||||
)
|
||||
|
||||
if not vmss_success and not vmsn_success:
|
||||
vmem_file_basename = os.path.basename(location)
|
||||
example_vmss_file_basename = os.path.basename(vmss)
|
||||
vollog.warning(
|
||||
f"No metadata file found alongside VMEM file. A VMSS or VMSN file may be required to correctly process a VMEM file. These should be placed in the same directory with the same file name, e.g. {vmem_file_basename} and {example_vmss_file_basename}.",
|
||||
)
|
||||
return None
|
||||
new_layer_name = context.layers.free_layer_name("VmwareLayer")
|
||||
context.config[
|
||||
|
||||
@@ -44,8 +44,9 @@ def array_of_pointers(
|
||||
raise TypeError(
|
||||
"Subtype must be a valid template (or string name of an object template)"
|
||||
)
|
||||
# We have to clone the pointer class, or we'll be defining the pointer subtype for all future pointers
|
||||
subtype_pointer = context.symbol_space.get_type(
|
||||
symbol_table + constants.BANG + "pointer"
|
||||
)
|
||||
).clone()
|
||||
subtype_pointer.update_vol(subtype=subtype)
|
||||
return array.cast("array", count=count, subtype=subtype_pointer)
|
||||
|
||||
@@ -75,11 +75,16 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
|
||||
bang_addrs = []
|
||||
|
||||
# get task memory sections to be used by scanners
|
||||
task_memory_sections = [
|
||||
section for section in task.get_process_memory_sections(heap_only=True)
|
||||
]
|
||||
|
||||
# find '#' values on the heap
|
||||
for address in proc_layer.scan(
|
||||
self.context,
|
||||
scanners.BytesScanner(b"#"),
|
||||
sections=task.get_process_memory_sections(heap_only=True),
|
||||
sections=task_memory_sections,
|
||||
):
|
||||
bang_addrs.append(struct.pack(pack_format, address))
|
||||
|
||||
@@ -89,7 +94,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
for address, _ in proc_layer.scan(
|
||||
self.context,
|
||||
scanners.MultiStringScanner(bang_addrs),
|
||||
sections=task.get_process_memory_sections(heap_only=True),
|
||||
sections=task_memory_sections,
|
||||
):
|
||||
hist = self.context.object(
|
||||
bash_table_name + constants.BANG + "hist_entry",
|
||||
|
||||
@@ -88,7 +88,7 @@ class Capabilities(plugins.PluginInterface):
|
||||
kernel_cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap")
|
||||
except exceptions.SymbolError:
|
||||
# It should be a kernel < 3.2
|
||||
return
|
||||
return None
|
||||
|
||||
vol2_last_cap = extensions.kernel_cap_struct.get_last_cap_value()
|
||||
if kernel_cap_last_cap > vol2_last_cap:
|
||||
|
||||
@@ -51,10 +51,22 @@ class Check_afinfo(plugins.PluginInterface):
|
||||
yield check, addr
|
||||
|
||||
def _check_afinfo(self, var_name, var, op_members, seq_members):
|
||||
for hooked_member, hook_address in self._check_members(
|
||||
var.seq_fops, var_name, op_members
|
||||
):
|
||||
yield var_name, hooked_member, hook_address
|
||||
# check if object has a least one of the members used for analysis by this function
|
||||
required_members = ["seq_fops", "seq_ops", "seq_show"]
|
||||
has_required_member = any(
|
||||
[var.has_member(member) for member in required_members]
|
||||
)
|
||||
if not has_required_member:
|
||||
vollog.debug(
|
||||
f"{var_name} object at {hex(var.vol.offset)} had none of the required members: {', '.join([member for member in required_members])}"
|
||||
)
|
||||
raise exceptions.PluginRequirementException
|
||||
|
||||
if var.has_member("seq_fops"):
|
||||
for hooked_member, hook_address in self._check_members(
|
||||
var.seq_fops, var_name, op_members
|
||||
):
|
||||
yield var_name, hooked_member, hook_address
|
||||
|
||||
# newer kernels
|
||||
if var.has_member("seq_ops"):
|
||||
@@ -64,8 +76,10 @@ class Check_afinfo(plugins.PluginInterface):
|
||||
yield var_name, hooked_member, hook_address
|
||||
|
||||
# this is the most commonly hooked member by rootkits, so a force a check on it
|
||||
elif not self._is_known_address(var.seq_show):
|
||||
yield var_name, "show", var.seq_show
|
||||
else:
|
||||
if var.has_member("seq_show"):
|
||||
if not self._is_known_address(var.seq_show):
|
||||
yield var_name, "show", var.seq_show
|
||||
|
||||
def _generator(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
@@ -85,6 +99,12 @@ class Check_afinfo(plugins.PluginInterface):
|
||||
)
|
||||
protocols = [tcp, udp]
|
||||
|
||||
# used to track the calls to _check_afinfo and the
|
||||
# number of errors produced due to missing members
|
||||
symbols_checked = set()
|
||||
symbols_with_errors = set()
|
||||
|
||||
# loop through all symbols
|
||||
for struct_type, global_vars in protocols:
|
||||
for global_var_name in global_vars:
|
||||
# this will lookup fail for the IPv6 protocols on kernels without IPv6 support
|
||||
@@ -97,10 +117,20 @@ class Check_afinfo(plugins.PluginInterface):
|
||||
object_type=struct_type, offset=global_var.address
|
||||
)
|
||||
|
||||
for name, member, address in self._check_afinfo(
|
||||
global_var_name, global_var, op_members, seq_members
|
||||
):
|
||||
yield 0, (name, member, format_hints.Hex(address))
|
||||
symbols_checked.add(global_var_name)
|
||||
try:
|
||||
for name, member, address in self._check_afinfo(
|
||||
global_var_name, global_var, op_members, seq_members
|
||||
):
|
||||
yield 0, (name, member, format_hints.Hex(address))
|
||||
except exceptions.PluginRequirementException:
|
||||
symbols_with_errors.add(global_var_name)
|
||||
|
||||
# if every call to _check_afinfo failed show a warning
|
||||
if symbols_checked == symbols_with_errors:
|
||||
vollog.warning(
|
||||
"This plugin was not able to check for hooks. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
|
||||
@@ -145,7 +145,7 @@ class Check_syscall(plugins.PluginInterface):
|
||||
table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz)
|
||||
except exceptions.SymbolError:
|
||||
vollog.error("Unable to find the system call table. Exiting.")
|
||||
return
|
||||
return None
|
||||
|
||||
tables = [(table_name, table_info)]
|
||||
|
||||
|
||||
@@ -4,20 +4,26 @@
|
||||
"""A module containing a collection of plugins that produce data typically
|
||||
found in Linux's /proc file system."""
|
||||
|
||||
from typing import List
|
||||
import logging
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework import constants, interfaces, renderers
|
||||
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
|
||||
from volatility3.framework.symbols import intermed
|
||||
from volatility3.framework.symbols.linux.extensions import elf
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Elfs(plugins.PluginInterface):
|
||||
"""Lists all memory mapped ELF files for all processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -36,9 +42,93 @@ class Elfs(plugins.PluginInterface):
|
||||
element_type=int,
|
||||
optional=True,
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Extract listed processes",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def elf_dump(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
layer_name: str,
|
||||
elf_table_name: str,
|
||||
vma: interfaces.objects.ObjectInterface,
|
||||
task: interfaces.objects.ObjectInterface,
|
||||
open_method: Type[interfaces.plugins.FileHandlerInterface],
|
||||
) -> Optional[interfaces.plugins.FileHandlerInterface]:
|
||||
"""Extracts an ELF as a FileHandlerInterface
|
||||
Args:
|
||||
context: the context to operate upon
|
||||
layer_name: The name of the layer on which to operate
|
||||
elf_table_name: the name for the symbol table containing the symbols for ELF-files
|
||||
vma: virtual memory allocation of ELF
|
||||
task: the task object whose memory should be output
|
||||
open_method: class to provide context manager for opening the file
|
||||
Returns:
|
||||
An open FileHandlerInterface object containing the complete data for the task or None in the case of failure
|
||||
"""
|
||||
|
||||
proc_layer = context.layers[layer_name]
|
||||
file_handle = None
|
||||
|
||||
elf_object = context.object(
|
||||
elf_table_name + constants.BANG + "Elf",
|
||||
offset=vma.vm_start,
|
||||
layer_name=layer_name,
|
||||
)
|
||||
|
||||
if not elf_object.is_valid():
|
||||
return None
|
||||
|
||||
sections = {}
|
||||
# TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ?
|
||||
for phdr in elf_object.get_program_headers():
|
||||
if phdr.p_type != 1: # PT_LOAD = 1
|
||||
continue
|
||||
|
||||
start = phdr.p_vaddr
|
||||
size = phdr.p_memsz
|
||||
end = start + size
|
||||
|
||||
# Use complete memory pages for dumping
|
||||
# If start isn't a multiple of 4096, stick to the highest multiple < start
|
||||
# If end isn't a multiple of 4096, stick to the lowest multiple > end
|
||||
if start % 4096:
|
||||
start = start & ~0xFFF
|
||||
|
||||
if end % 4096:
|
||||
end = (end & ~0xFFF) + 4096
|
||||
|
||||
real_size = end - start
|
||||
|
||||
# Check if ELF has a legitimate size
|
||||
if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE:
|
||||
raise ValueError(f"The claimed size of the ELF is invalid: {real_size}")
|
||||
|
||||
sections[start] = real_size
|
||||
|
||||
elf_data = b""
|
||||
for section_start in sorted(sections.keys()):
|
||||
read_size = sections[section_start]
|
||||
|
||||
buf = proc_layer.read(vma.vm_start + section_start, read_size, pad=True)
|
||||
elf_data = elf_data + buf
|
||||
|
||||
file_handle = open_method(
|
||||
f"pid.{task.pid}.{utility.array_to_string(task.comm)}.{vma.vm_start:#x}.dmp"
|
||||
)
|
||||
file_handle.write(elf_data)
|
||||
|
||||
return file_handle
|
||||
|
||||
def _generator(self, tasks):
|
||||
elf_table_name = intermed.IntermediateSymbolTable.create(
|
||||
self.context, self.config_path, "linux", "elf", class_types=elf.class_types
|
||||
)
|
||||
for task in tasks:
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if not proc_layer_name:
|
||||
@@ -60,6 +150,21 @@ class Elfs(plugins.PluginInterface):
|
||||
|
||||
path = vma.get_name(self.context, task)
|
||||
|
||||
file_output = "Disabled"
|
||||
if self.config["dump"]:
|
||||
file_handle = self.elf_dump(
|
||||
self.context,
|
||||
proc_layer_name,
|
||||
elf_table_name,
|
||||
vma,
|
||||
task,
|
||||
self.open,
|
||||
)
|
||||
file_output = "Error outputting file"
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
file_output = str(file_handle.preferred_filename)
|
||||
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
@@ -68,6 +173,7 @@ class Elfs(plugins.PluginInterface):
|
||||
format_hints.Hex(vma.vm_start),
|
||||
format_hints.Hex(vma.vm_end),
|
||||
path,
|
||||
file_output,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -81,6 +187,7 @@ class Elfs(plugins.PluginInterface):
|
||||
("Start", format_hints.Hex),
|
||||
("End", format_hints.Hex),
|
||||
("File Path", str),
|
||||
("File Output", str),
|
||||
],
|
||||
self._generator(
|
||||
pslist.PsList.list_tasks(
|
||||
|
||||
@@ -16,7 +16,7 @@ class IOMem(interfaces.plugins.PluginInterface):
|
||||
"""Generates an output similar to /proc/iomem on a running system."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 1)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -53,7 +53,7 @@ class IOMem(interfaces.plugins.PluginInterface):
|
||||
|
||||
# create the resource object with protection against memory smear
|
||||
try:
|
||||
resource = vmlinux.object("resource", resource_offset)
|
||||
resource = vmlinux.object("resource", resource_offset, absolute=True)
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.warning(
|
||||
f"Unable to create resource object at {resource_offset:#x}. This resource, "
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
from typing import List
|
||||
|
||||
import logging
|
||||
from volatility3.framework import constants, interfaces
|
||||
from volatility3.framework import renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
@@ -11,6 +11,8 @@ from volatility3.framework.objects import utility
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Malfind(interfaces.plugins.PluginInterface):
|
||||
"""Lists process memory ranges that potentially contain injected code."""
|
||||
@@ -42,12 +44,19 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if not proc_layer_name:
|
||||
return
|
||||
return None
|
||||
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
for vma in task.mm.get_vma_iter():
|
||||
if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]":
|
||||
vma_name = vma.get_name(self.context, task)
|
||||
vollog.debug(
|
||||
f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}"
|
||||
)
|
||||
if (
|
||||
vma.is_suspicious(proc_layer)
|
||||
and vma.get_name(self.context, task) != "[vdso]"
|
||||
):
|
||||
data = proc_layer.read(vma.vm_start, 64, pad=True)
|
||||
yield vma, data
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# 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
|
||||
#
|
||||
from typing import Callable, Iterable, List, Any, Tuple
|
||||
from typing import Any, Callable, Iterable, List
|
||||
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework import interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.symbols import intermed
|
||||
from volatility3.framework.symbols.linux.extensions import elf
|
||||
from volatility3.plugins.linux import elfs
|
||||
|
||||
|
||||
class PsList(interfaces.plugins.PluginInterface):
|
||||
@@ -24,6 +27,9 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="elfs", plugin=elfs.Elfs, version=(2, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
description="Filter on specific process IDs",
|
||||
@@ -42,6 +48,12 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
optional=True,
|
||||
default=False,
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Extract listed processes",
|
||||
optional=True,
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@@ -66,38 +78,12 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
else:
|
||||
return lambda _: False
|
||||
|
||||
def _get_task_fields(
|
||||
self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
|
||||
) -> Tuple[int, int, int, str]:
|
||||
"""Extract the fields needed for the final output
|
||||
|
||||
Args:
|
||||
task: A task object from where to get the fields.
|
||||
decorate_comm: If True, it decorates the comm string of
|
||||
- User threads: in curly brackets,
|
||||
- Kernel threads: in square brackets
|
||||
Defaults to False.
|
||||
Returns:
|
||||
A tuple with the fields to show in the plugin output.
|
||||
"""
|
||||
pid = task.tgid
|
||||
tid = task.pid
|
||||
ppid = task.parent.tgid if task.parent else 0
|
||||
name = utility.array_to_string(task.comm)
|
||||
if decorate_comm:
|
||||
if task.is_kernel_thread:
|
||||
name = f"[{name}]"
|
||||
elif task.is_user_thread:
|
||||
name = f"{{{name}}}"
|
||||
|
||||
task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name)
|
||||
return task_fields
|
||||
|
||||
def _generator(
|
||||
self,
|
||||
pid_filter: Callable[[Any], bool],
|
||||
include_threads: bool = False,
|
||||
decorate_comm: bool = False,
|
||||
dump: bool = False,
|
||||
):
|
||||
"""Generates the tasks list.
|
||||
|
||||
@@ -110,14 +96,63 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
- User threads: in curly brackets,
|
||||
- Kernel threads: in square brackets
|
||||
Defaults to False.
|
||||
dump: If True, the main executable of the process is written to a file
|
||||
Defaults to False.
|
||||
Yields:
|
||||
Each rows
|
||||
"""
|
||||
for task in self.list_tasks(
|
||||
self.context, self.config["kernel"], pid_filter, include_threads
|
||||
):
|
||||
row = self._get_task_fields(task, decorate_comm)
|
||||
yield (0, row)
|
||||
elf_table_name = intermed.IntermediateSymbolTable.create(
|
||||
self.context,
|
||||
self.config_path,
|
||||
"linux",
|
||||
"elf",
|
||||
class_types=elf.class_types,
|
||||
)
|
||||
file_output = "Disabled"
|
||||
if dump:
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if not proc_layer_name:
|
||||
continue
|
||||
|
||||
# Find the vma that belongs to the main ELF of the process
|
||||
file_output = "Error outputting file"
|
||||
|
||||
for v in task.mm.get_mmap_iter():
|
||||
if v.vm_start == task.mm.start_code:
|
||||
file_handle = elfs.Elfs.elf_dump(
|
||||
self.context,
|
||||
proc_layer_name,
|
||||
elf_table_name,
|
||||
v,
|
||||
task,
|
||||
self.open,
|
||||
)
|
||||
if file_handle:
|
||||
file_output = str(file_handle.preferred_filename)
|
||||
file_handle.close()
|
||||
break
|
||||
|
||||
pid = task.tgid
|
||||
tid = task.pid
|
||||
ppid = task.parent.tgid if task.parent else 0
|
||||
name = utility.array_to_string(task.comm)
|
||||
if decorate_comm:
|
||||
if task.is_kernel_thread:
|
||||
name = f"[{name}]"
|
||||
elif task.is_user_thread:
|
||||
name = f"{{{name}}}"
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(task.vol.offset),
|
||||
pid,
|
||||
tid,
|
||||
ppid,
|
||||
name,
|
||||
file_output,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list_tasks(
|
||||
@@ -155,6 +190,7 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
pids = self.config.get("pid")
|
||||
include_threads = self.config.get("threads")
|
||||
decorate_comm = self.config.get("decorate_comm")
|
||||
dump = self.config.get("dump")
|
||||
filter_func = self.create_pid_filter(pids)
|
||||
|
||||
columns = [
|
||||
@@ -163,7 +199,8 @@ class PsList(interfaces.plugins.PluginInterface):
|
||||
("TID", int),
|
||||
("PPID", int),
|
||||
("COMM", str),
|
||||
("File output", str),
|
||||
]
|
||||
return renderers.TreeGrid(
|
||||
columns, self._generator(filter_func, include_threads, decorate_comm)
|
||||
columns, self._generator(filter_func, include_threads, decorate_comm, dump)
|
||||
)
|
||||
|
||||
@@ -147,7 +147,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
socket_filter["bpf_filter_type"] = "cBPF"
|
||||
|
||||
if not sock_filter.has_member("prog") or not sock_filter.prog:
|
||||
return
|
||||
return None
|
||||
|
||||
bpfprog = sock_filter.prog
|
||||
|
||||
@@ -158,13 +158,13 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
return # cBPF filter
|
||||
except AttributeError:
|
||||
# kernel < 3.18.140, it's a cBPF filter
|
||||
return
|
||||
return None
|
||||
|
||||
BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter
|
||||
if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER:
|
||||
socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})"
|
||||
vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket")
|
||||
return
|
||||
return None
|
||||
|
||||
socket_filter["bpf_filter_type"] = "eBPF"
|
||||
if not bpfprog.has_member("aux") or not bpfprog.aux:
|
||||
@@ -329,17 +329,17 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
xdp_sock = sock.cast("xdp_sock")
|
||||
device = xdp_sock.dev
|
||||
if not device:
|
||||
return
|
||||
return None
|
||||
|
||||
src_addr = utility.array_to_string(device.name)
|
||||
src_port = dst_addr = dst_port = None
|
||||
|
||||
bpfprog = device.xdp_prog
|
||||
if not bpfprog:
|
||||
return
|
||||
return None
|
||||
|
||||
if not bpfprog.has_member("aux") or not bpfprog.aux:
|
||||
return
|
||||
return None
|
||||
|
||||
bpfprog_aux = bpfprog.aux
|
||||
if bpfprog_aux.has_member("id"):
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# This file is Copyright 2023 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 Iterable, List, Tuple
|
||||
|
||||
from volatility3.framework import interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.plugins import yarascan
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
|
||||
class VmaYaraScan(interfaces.plugins.PluginInterface):
|
||||
"""Scans all virtual memory areas for tasks using yara."""
|
||||
|
||||
_required_framework_version = (2, 4, 0)
|
||||
_version = (1, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
# create a list of requirements for vmayarascan
|
||||
vmayarascan_requirements = [
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
element_type=int,
|
||||
description="Process IDs to include (all other processes are excluded)",
|
||||
optional=True,
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
|
||||
),
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
]
|
||||
|
||||
# get base yarascan requirements for command line options
|
||||
yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements()
|
||||
|
||||
# return the combined requirements
|
||||
return yarascan_requirements + vmayarascan_requirements
|
||||
|
||||
def _generator(self):
|
||||
# use yarascan to parse the yara options provided and create the rules
|
||||
rules = yarascan.YaraScan.process_yara_options(dict(self.config))
|
||||
|
||||
# filter based on the pid option if provided
|
||||
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
|
||||
for task in pslist.PsList.list_tasks(
|
||||
context=self.context,
|
||||
vmlinux_module_name=self.config["kernel"],
|
||||
filter_func=filter_func,
|
||||
):
|
||||
# attempt to create a process layer for each task and skip those
|
||||
# that cannot (e.g. kernel threads)
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if not proc_layer_name:
|
||||
continue
|
||||
|
||||
# get the proc_layer object from the context
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
# scan the process layer with the yarascanner
|
||||
for offset, rule_name, name, value in proc_layer.scan(
|
||||
context=self.context,
|
||||
scanner=yarascan.YaraScanner(rules=rules),
|
||||
sections=self.get_vma_maps(task),
|
||||
):
|
||||
yield 0, (
|
||||
format_hints.Hex(offset),
|
||||
task.tgid,
|
||||
rule_name,
|
||||
name,
|
||||
value,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_vma_maps(
|
||||
task: interfaces.objects.ObjectInterface,
|
||||
) -> Iterable[Tuple[int, int]]:
|
||||
"""Creates a map of start/end addresses for each virtual memory area in a task.
|
||||
|
||||
Args:
|
||||
task: The task object of which to read the vmas from
|
||||
|
||||
Returns:
|
||||
An iterable of tuples containing start and end addresses for each descriptor
|
||||
"""
|
||||
if task.mm:
|
||||
for vma in task.mm.get_vma_iter():
|
||||
vm_size = vma.vm_end - vma.vm_start
|
||||
yield (vma.vm_start, vm_size)
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Offset", format_hints.Hex),
|
||||
("PID", int),
|
||||
("Rule", str),
|
||||
("Component", str),
|
||||
("Value", bytes),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
@@ -69,7 +69,7 @@ class Check_sysctl(plugins.PluginInterface):
|
||||
try:
|
||||
sysctl = sysctl.oid_link.sle_next.dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
while sysctl:
|
||||
try:
|
||||
|
||||
@@ -116,7 +116,7 @@ class Kevents(interfaces.plugins.PluginInterface):
|
||||
)
|
||||
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
for klist in klist_array:
|
||||
for kn in mac.MacUtilities.walk_slist(klist, "kn_link"):
|
||||
@@ -140,7 +140,7 @@ class Kevents(interfaces.plugins.PluginInterface):
|
||||
try:
|
||||
p_klist = task.p_klist
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
for kn in mac.MacUtilities.walk_slist(p_klist, "kn_link"):
|
||||
yield kn
|
||||
|
||||
@@ -75,7 +75,7 @@ class Lsmod(plugins.PluginInterface):
|
||||
try:
|
||||
kmod = kmod.next
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
return # Generation finished
|
||||
|
||||
def _generator(self):
|
||||
|
||||
@@ -40,7 +40,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if proc_layer_name is None:
|
||||
return
|
||||
return None
|
||||
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
|
||||
@@ -108,12 +108,12 @@ class Cachedump(interfaces.plugins.PluginInterface):
|
||||
vollog.warning("Unable to locate SYSTEM hive")
|
||||
if sechive is None:
|
||||
vollog.warning("Unable to locate SECURITY hive")
|
||||
return
|
||||
return None
|
||||
|
||||
bootkey = hashdump.Hashdump.get_bootkey(syshive)
|
||||
if not bootkey:
|
||||
vollog.warning("Unable to find bootkey")
|
||||
return
|
||||
return None
|
||||
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
|
||||
@@ -124,17 +124,17 @@ class Cachedump(interfaces.plugins.PluginInterface):
|
||||
lsakey = lsadump.Lsadump.get_lsa_key(sechive, bootkey, vista_or_later)
|
||||
if not lsakey:
|
||||
vollog.warning("Unable to find lsa key")
|
||||
return
|
||||
return None
|
||||
|
||||
nlkm = self.get_nlkm(sechive, lsakey, vista_or_later)
|
||||
if not nlkm:
|
||||
vollog.warning("Unable to find nlkma key")
|
||||
return
|
||||
return None
|
||||
|
||||
cache = hashdump.Hashdump.get_hive_key(sechive, "Cache")
|
||||
if not cache:
|
||||
vollog.warning("Unable to find cache key")
|
||||
return
|
||||
return None
|
||||
|
||||
for cache_item in cache.get_values():
|
||||
if cache_item.Name == "NL$Control":
|
||||
|
||||
@@ -157,7 +157,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
)
|
||||
|
||||
if callback_count == 0:
|
||||
return
|
||||
return None
|
||||
|
||||
fast_refs = ntkrnlmp.object(
|
||||
object_type="array",
|
||||
@@ -199,7 +199,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
)
|
||||
|
||||
if callback_count == 0:
|
||||
return
|
||||
return None
|
||||
|
||||
callback_list = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=symbol_offset)
|
||||
for callback in callback_list.to_list(full_type_name, "Link"):
|
||||
@@ -256,7 +256,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
symbol_status = "exists"
|
||||
vollog.debug(f"symbol {symbol_name} {symbol_status}.")
|
||||
|
||||
return
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def list_bugcheck_reason_callbacks(
|
||||
@@ -287,7 +287,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
).address
|
||||
except exceptions.SymbolError:
|
||||
vollog.debug("Cannot find KeBugCheckReasonCallbackListHead")
|
||||
return
|
||||
return None
|
||||
|
||||
full_type_name = (
|
||||
callback_table_name + constants.BANG + "_KBUGCHECK_REASON_CALLBACK_RECORD"
|
||||
@@ -343,7 +343,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
list_offset = ntkrnlmp.get_symbol("KeBugCheckCallbackListHead").address
|
||||
except exceptions.SymbolError:
|
||||
vollog.debug("Cannot find KeBugCheckCallbackListHead")
|
||||
return
|
||||
return None
|
||||
|
||||
full_type_name = (
|
||||
callback_table_name + constants.BANG + "_KBUGCHECK_CALLBACK_RECORD"
|
||||
|
||||
@@ -130,7 +130,7 @@ class DumpFiles(interfaces.plugins.PluginInterface):
|
||||
constants.LOGLEVEL_VVV,
|
||||
f"The file object at {file_obj.vol.offset:#x} is not a file on disk",
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
# Depending on the type of object (DataSection, ImageSection, SharedCacheMap) we may need to
|
||||
# read from the memory layer or the primary layer.
|
||||
|
||||
@@ -285,7 +285,7 @@ class Handles(interfaces.plugins.PluginInterface):
|
||||
count = 0x1000 / subtype.size
|
||||
|
||||
if not self.context.layers[virtual].is_valid(offset):
|
||||
return
|
||||
return None
|
||||
|
||||
table = ntkrnlmp.object(
|
||||
object_type="array",
|
||||
@@ -335,7 +335,7 @@ class Handles(interfaces.plugins.PluginInterface):
|
||||
constants.LOGLEVEL_VVV,
|
||||
"Handle table parsing was aborted due to an invalid address exception",
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
for handle_table_entry in self._make_handle_array(TableCode, table_levels):
|
||||
yield handle_table_entry
|
||||
|
||||
@@ -168,16 +168,16 @@ class Lsadump(interfaces.plugins.PluginInterface):
|
||||
lsakey = self.get_lsa_key(sechive, bootkey, vista_or_later)
|
||||
if not bootkey:
|
||||
vollog.warning("Unable to find bootkey")
|
||||
return
|
||||
return None
|
||||
|
||||
if not lsakey:
|
||||
vollog.warning("Unable to find lsa key")
|
||||
return
|
||||
return None
|
||||
|
||||
secrets_key = hashdump.Hashdump.get_hive_key(sechive, "Policy\\Secrets")
|
||||
if not secrets_key:
|
||||
vollog.warning("Unable to find secrets key")
|
||||
return
|
||||
return None
|
||||
|
||||
for key in secrets_key.get_subkeys():
|
||||
sec_val_key = hashdump.Hashdump.get_hive_key(
|
||||
|
||||
@@ -110,7 +110,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
proc_id, excp.invalid_address, excp.layer_name
|
||||
)
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
proc_layer = context.layers[proc_layer_name]
|
||||
|
||||
|
||||
@@ -67,9 +67,8 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
# We will update this on each pass in the next loop and use it as the new offset.
|
||||
attr_base_offset = mft_record.FirstAttrOffset
|
||||
|
||||
attr_header = self.context.object(
|
||||
header_object,
|
||||
attr = self.context.object(
|
||||
attribute_object,
|
||||
offset=offset + attr_base_offset,
|
||||
layer_name=layer.name,
|
||||
)
|
||||
@@ -77,17 +76,8 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
# There is no field that has a count of Attributes
|
||||
# Keep Attempting to read attributes until we get an invalid attr_header.AttrType
|
||||
|
||||
while attr_header.AttrType.is_valid_choice:
|
||||
vollog.debug(f"Attr Type: {attr_header.AttrType.lookup()}")
|
||||
|
||||
# Offset past the headers to the attribute data
|
||||
attr_data_offset = (
|
||||
offset
|
||||
+ attr_base_offset
|
||||
+ self.context.symbol_space.get_type(
|
||||
attribute_object
|
||||
).relative_child_offset("Attr_Data")
|
||||
)
|
||||
while attr.Attr_Header.AttrType.is_valid_choice:
|
||||
vollog.debug(f"Attr Type: {attr.Attr_Header.AttrType.lookup()}")
|
||||
|
||||
# MFT Flags determine the file type or dir
|
||||
# If we don't have a valid enum, coerce to hex so we can keep the record
|
||||
@@ -97,19 +87,16 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
mft_flag = hex(mft_record.Flags)
|
||||
|
||||
# Standard Information Attribute
|
||||
if attr_header.AttrType.lookup() == "STANDARD_INFORMATION":
|
||||
attr_data = self.context.object(
|
||||
si_object, offset=attr_data_offset, layer_name=layer.name
|
||||
)
|
||||
|
||||
if attr.Attr_Header.AttrType.lookup() == "STANDARD_INFORMATION":
|
||||
attr_data = attr.Attr_Data.cast(si_object)
|
||||
yield 0, (
|
||||
format_hints.Hex(attr_data_offset),
|
||||
format_hints.Hex(attr_data.vol.offset),
|
||||
mft_record.get_signature(),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
renderers.NotApplicableValue(),
|
||||
attr_header.AttrType.lookup(),
|
||||
attr.Attr_Header.AttrType.lookup(),
|
||||
conversion.wintime_to_datetime(attr_data.CreationTime),
|
||||
conversion.wintime_to_datetime(attr_data.ModifiedTime),
|
||||
conversion.wintime_to_datetime(attr_data.UpdatedTime),
|
||||
@@ -118,10 +105,8 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
|
||||
# File Name Attribute
|
||||
if attr_header.AttrType.lookup() == "FILE_NAME":
|
||||
attr_data = self.context.object(
|
||||
fn_object, offset=attr_data_offset, layer_name=layer.name
|
||||
)
|
||||
if attr.Attr_Header.AttrType.lookup() == "FILE_NAME":
|
||||
attr_data = attr.Attr_Data.cast(fn_object)
|
||||
file_name = attr_data.get_full_name()
|
||||
|
||||
# If we don't have a valid enum, coerce to hex so we can keep the record
|
||||
@@ -131,13 +116,13 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
permissions = hex(attr_data.Flags)
|
||||
|
||||
yield 1, (
|
||||
format_hints.Hex(attr_data_offset),
|
||||
format_hints.Hex(attr_data.vol.offset),
|
||||
mft_record.get_signature(),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
permissions,
|
||||
attr_header.AttrType.lookup(),
|
||||
attr.Attr_Header.AttrType.lookup(),
|
||||
conversion.wintime_to_datetime(attr_data.CreationTime),
|
||||
conversion.wintime_to_datetime(attr_data.ModifiedTime),
|
||||
conversion.wintime_to_datetime(attr_data.UpdatedTime),
|
||||
@@ -146,14 +131,13 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
|
||||
# If there's no advancement the loop will never end, so break it now
|
||||
if attr_header.Length == 0:
|
||||
if attr.Attr_Header.Length == 0:
|
||||
break
|
||||
|
||||
# Update the base offset to point to the next attribute
|
||||
attr_base_offset += attr_header.Length
|
||||
# Get the next attribute
|
||||
attr_header = self.context.object(
|
||||
header_object,
|
||||
attr_base_offset += attr.Attr_Header.Length
|
||||
attr = self.context.object(
|
||||
attribute_object,
|
||||
offset=offset + attr_base_offset,
|
||||
layer_name=layer.name,
|
||||
)
|
||||
@@ -189,3 +173,140 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
|
||||
class ADS(interfaces.plugins.PluginInterface):
|
||||
|
||||
"""Scans for Alternate Data Stream"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
return [
|
||||
requirements.TranslationLayerRequirement(
|
||||
name="primary",
|
||||
description="Memory layer for the kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
|
||||
),
|
||||
]
|
||||
|
||||
def _generator(self):
|
||||
layer = self.context.layers[self.config["primary"]]
|
||||
|
||||
# Yara Rule to scan for MFT Header Signatures
|
||||
rules = yarascan.YaraScan.process_yara_options(
|
||||
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
|
||||
)
|
||||
|
||||
# Read in the Symbol File
|
||||
symbol_table = intermed.IntermediateSymbolTable.create(
|
||||
context=self.context,
|
||||
config_path=self.config_path,
|
||||
sub_path="windows",
|
||||
filename="mft",
|
||||
class_types={
|
||||
"MFT_ENTRY": mft.MFTEntry,
|
||||
"FILE_NAME_ENTRY": mft.MFTFileName,
|
||||
"ATTRIBUTE": mft.MFTAttribute,
|
||||
},
|
||||
)
|
||||
|
||||
# get each of the individual Field Sets
|
||||
mft_object = symbol_table + constants.BANG + "MFT_ENTRY"
|
||||
attribute_object = symbol_table + constants.BANG + "ATTRIBUTE"
|
||||
fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY"
|
||||
|
||||
# Scan the layer for Raw MFT records and parse the fields
|
||||
for offset, _rule_name, _name, _value in layer.scan(
|
||||
context=self.context, scanner=yarascan.YaraScanner(rules=rules)
|
||||
):
|
||||
with contextlib.suppress(exceptions.PagedInvalidAddressException):
|
||||
mft_record = self.context.object(
|
||||
mft_object, offset=offset, layer_name=layer.name
|
||||
)
|
||||
# We will update this on each pass in the next loop and use it as the new offset.
|
||||
attr_base_offset = mft_record.FirstAttrOffset
|
||||
|
||||
attr = self.context.object(
|
||||
attribute_object,
|
||||
offset=offset + attr_base_offset,
|
||||
layer_name=layer.name,
|
||||
)
|
||||
|
||||
# There is no field that has a count of Attributes
|
||||
# Keep Attempting to read attributes until we get an invalid attr.AttrType
|
||||
is_ads = False
|
||||
file_name = renderers.NotAvailableValue
|
||||
# The First $DATA Attr is the 'principal' file itself not the ADS
|
||||
while attr.Attr_Header.AttrType.is_valid_choice:
|
||||
if attr.Attr_Header.AttrType.lookup() == "FILE_NAME":
|
||||
attr_data = attr.Attr_Data.cast(fn_object)
|
||||
file_name = attr_data.get_full_name()
|
||||
if attr.Attr_Header.AttrType.lookup() == "DATA":
|
||||
if is_ads:
|
||||
if not attr.Attr_Header.NonResidentFlag:
|
||||
# Resident files are the most interesting.
|
||||
if attr.Attr_Header.NameLength > 0:
|
||||
ads_name = attr.get_resident_filename()
|
||||
if not ads_name:
|
||||
ads_name = renderers.NotAvailableValue
|
||||
|
||||
content = attr.get_resident_filecontent()
|
||||
if content:
|
||||
# Preparing for Disassembly
|
||||
disasm = interfaces.renderers.BaseAbsentValue
|
||||
architecture = layer.metadata.get(
|
||||
"architecture", None
|
||||
)
|
||||
if architecture:
|
||||
disasm = interfaces.renderers.Disassembly(
|
||||
content, 0, architecture.lower()
|
||||
)
|
||||
else:
|
||||
content = renderers.NotAvailableValue
|
||||
disasm = interfaces.renderers.BaseAbsentValue
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(attr_data.vol.offset),
|
||||
mft_record.get_signature(),
|
||||
mft_record.RecordNumber,
|
||||
attr.Attr_Header.AttrType.lookup(),
|
||||
file_name,
|
||||
ads_name,
|
||||
format_hints.HexBytes(content),
|
||||
disasm,
|
||||
)
|
||||
else:
|
||||
is_ads = True
|
||||
|
||||
# If there's no advancement the loop will never end, so break it now
|
||||
if attr.Attr_Header.Length == 0:
|
||||
break
|
||||
|
||||
# Update the base offset to point to the next attribute
|
||||
attr_base_offset += attr.Attr_Header.Length
|
||||
# Get the next attribute
|
||||
attr = self.context.object(
|
||||
attribute_object,
|
||||
offset=offset + attr_base_offset,
|
||||
layer_name=layer.name,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Offset", format_hints.Hex),
|
||||
("Record Type", str),
|
||||
("Record Number", int),
|
||||
("MFT Type", str),
|
||||
("Filename", str),
|
||||
("ADS Filename", str),
|
||||
("Hexdump", format_hints.HexBytes),
|
||||
("Disasm", interfaces.renderers.Disassembly),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
@@ -154,7 +154,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
else:
|
||||
# invalid argument.
|
||||
return
|
||||
return None
|
||||
|
||||
vollog.debug(f"Current Port: {port}")
|
||||
# the given port serves as a shifted index into the port pool lists
|
||||
@@ -175,7 +175,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
assignment = inpa.InPaBigPoolBase.Assignments[truncated_port]
|
||||
|
||||
if not assignment:
|
||||
return
|
||||
return None
|
||||
|
||||
# the value within assignment.Entry is a) masked and b) points inside of the network object
|
||||
# first decode the pointer
|
||||
|
||||
@@ -90,9 +90,19 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
offset=peb.ImageBaseAddress,
|
||||
layer_name=proc_layer_name,
|
||||
)
|
||||
file_handle = open_method(
|
||||
f"pid.{proc.UniqueProcessId}.{peb.ImageBaseAddress:#x}.dmp"
|
||||
|
||||
process_name = proc.ImageFileName.cast(
|
||||
"string",
|
||||
max_length=proc.ImageFileName.vol.count,
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
file_handle = open_method(
|
||||
open_method.sanitize_filename(
|
||||
f"{proc.UniqueProcessId}.{process_name}.{peb.ImageBaseAddress:#x}.dmp"
|
||||
)
|
||||
)
|
||||
|
||||
for offset, data in dos_header.reconstruct():
|
||||
file_handle.seek(offset)
|
||||
file_handle.write(data)
|
||||
|
||||
@@ -108,13 +108,13 @@ class PsTree(interfaces.plugins.PluginInterface):
|
||||
def yield_processes(pid, descendant: bool = False):
|
||||
if pid in process_pids:
|
||||
vollog.debug(f"Pid cycle: already processed pid {pid}")
|
||||
return
|
||||
return None
|
||||
|
||||
process_pids.add(pid)
|
||||
|
||||
if pid not in self._ancestors and not descendant:
|
||||
vollog.debug(f"Pid cycle: pid {pid} not in filtered tree")
|
||||
return
|
||||
return None
|
||||
|
||||
proc, offset = self._processes[pid]
|
||||
row = (
|
||||
|
||||
@@ -30,7 +30,7 @@ class HiveGenerator:
|
||||
):
|
||||
if not hive.is_valid():
|
||||
self._invalid = hive.vol.offset
|
||||
return
|
||||
return None
|
||||
yield hive
|
||||
|
||||
@property
|
||||
|
||||
@@ -74,7 +74,7 @@ class PrintKey(interfaces.plugins.PluginInterface):
|
||||
node_path = [hive.get_node(hive.root_cell_offset)]
|
||||
if not isinstance(node_path, list) or len(node_path) < 1:
|
||||
vollog.warning("Hive walker was not passed a valid node_path (or None)")
|
||||
return
|
||||
return None
|
||||
node = node_path[-1]
|
||||
key_path_items = [hive] + node_path[1:]
|
||||
key_path = "\\".join([k.get_name() for k in key_path_items])
|
||||
@@ -153,6 +153,11 @@ class PrintKey(interfaces.plugins.PluginInterface):
|
||||
vollog.debug(excp)
|
||||
key_node_name = renderers.UnreadableValue()
|
||||
|
||||
# if the item is a subkey, use the LastWriteTime of that subkey
|
||||
last_write_time = conversion.wintime_to_datetime(
|
||||
node.LastWriteTime.QuadPart
|
||||
)
|
||||
|
||||
yield (
|
||||
depth,
|
||||
(
|
||||
|
||||
@@ -173,11 +173,11 @@ class UserAssist(interfaces.plugins.PluginInterface):
|
||||
|
||||
if not userassist_node_path:
|
||||
vollog.warning("list_userassist did not find a valid node_path (or None)")
|
||||
return
|
||||
return None
|
||||
|
||||
if not isinstance(userassist_node_path, list):
|
||||
vollog.warning("userassist_node_path did not return a list as expected")
|
||||
return
|
||||
return None
|
||||
userassist_node = userassist_node_path[-1]
|
||||
# iterate through the GUIDs under the userassist key
|
||||
for guidkey in userassist_node.get_subkeys():
|
||||
|
||||
@@ -601,21 +601,21 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
|
||||
|
||||
if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name):
|
||||
vollog.info("This plugin only supports 64bit Windows memory samples")
|
||||
return
|
||||
return None
|
||||
|
||||
lsass_proc, proc_layer_name = self._find_lsass_proc(procs)
|
||||
if not lsass_proc:
|
||||
vollog.info(
|
||||
"Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed."
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc)
|
||||
if not cryptdll_base:
|
||||
vollog.info(
|
||||
"Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed."
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
# the custom type information from binary analysis
|
||||
cryptdll_types = self._get_cryptdll_types(
|
||||
@@ -649,7 +649,7 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
|
||||
vollog.info(
|
||||
"Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed."
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
for csystem in csystems:
|
||||
if not self.context.layers[proc_layer_name].is_valid(
|
||||
|
||||
@@ -18,47 +18,26 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
"""Scans all the Virtual Address Descriptor memory maps using yara."""
|
||||
|
||||
_required_framework_version = (2, 4, 0)
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 1)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
# create a list of requirements for vadyarascan
|
||||
vadyarascan_requirements = [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Windows kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="wide",
|
||||
description="Match wide (unicode) strings",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="yara_rules", description="Yara rules (as a string)", optional=True
|
||||
),
|
||||
requirements.URIRequirement(
|
||||
name="yara_file", description="Yara rules (as a file)", optional=True
|
||||
),
|
||||
# This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code
|
||||
# As such, there's a separate option to run compiled files, as happened with yara-3.9 and later
|
||||
requirements.URIRequirement(
|
||||
name="yara_compiled_file",
|
||||
description="Yara compiled rules (as a file)",
|
||||
optional=True,
|
||||
),
|
||||
requirements.IntRequirement(
|
||||
name="max_size",
|
||||
default=0x40000000,
|
||||
description="Set the maximum size (default is 1GB)",
|
||||
optional=True,
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
element_type=int,
|
||||
@@ -67,6 +46,12 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
),
|
||||
]
|
||||
|
||||
# get base yarascan requirements for command line options
|
||||
yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements()
|
||||
|
||||
# return the combined requirements
|
||||
return yarascan_requirements + vadyarascan_requirements
|
||||
|
||||
def _generator(self):
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
|
||||
|
||||
@@ -61,19 +61,31 @@ class YaraScan(plugins.PluginInterface):
|
||||
"""Scans kernel memory using yara rules (string or file)."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 1, 0)
|
||||
_version = (1, 2, 0)
|
||||
|
||||
# TODO: When the major version is bumped, take the opportunity to rename the yara_rules config to yara_string
|
||||
# or something that makes more sense
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
"""Returns the requirements needed to run yarascan directly, combining the TranslationLayerRequirement
|
||||
and the requirements from get_yarascan_option_requirements."""
|
||||
return cls.get_yarascan_option_requirements() + [
|
||||
requirements.TranslationLayerRequirement(
|
||||
name="primary",
|
||||
description="Memory layer for the kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_yarascan_option_requirements(
|
||||
cls,
|
||||
) -> List[interfaces.configuration.RequirementInterface]:
|
||||
"""Returns the requirements needed for the command lines options used by yarascan. This can
|
||||
then also be used by other plugins that are using yarascan. This does not include a
|
||||
TranslationLayerRequirement or a ModuleRequirement."""
|
||||
return [
|
||||
requirements.BooleanRequirement(
|
||||
name="insensitive",
|
||||
description="Makes the search case insensitive",
|
||||
|
||||
@@ -10,7 +10,7 @@ import collections
|
||||
import collections.abc
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar, Union
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, Union
|
||||
|
||||
from volatility3.framework import interfaces
|
||||
from volatility3.framework.interfaces import renderers
|
||||
@@ -96,6 +96,10 @@ class TreeNode(interfaces.renderers.TreeNode):
|
||||
# if isinstance(val, datetime.datetime):
|
||||
# tznaive = val.tzinfo is None or val.tzinfo.utcoffset(val) is None
|
||||
|
||||
def asdict(self) -> Dict[str, Any]:
|
||||
"""Returns the contents of the node as a dictionary"""
|
||||
return self._values._asdict()
|
||||
|
||||
@property
|
||||
def values(self) -> List[interfaces.renderers.BaseTypes]:
|
||||
"""Returns the list of values from the particular node, based on column
|
||||
|
||||
@@ -59,7 +59,8 @@ class MultiTypeData(bytes):
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
super(self) == super(other)
|
||||
isinstance(other, self.__class__)
|
||||
and super() == super(self.__class__, other)
|
||||
and self.converted_int == other.converted_int
|
||||
and self.encoding == other.encoding
|
||||
and self.split_nulls == other.split_nulls
|
||||
|
||||
@@ -29,6 +29,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
self.set_type_class("files_struct", extensions.files_struct)
|
||||
self.set_type_class("kobject", extensions.kobject)
|
||||
self.set_type_class("cred", extensions.cred)
|
||||
self.set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
|
||||
# Might not exist in the current symbols
|
||||
self.optional_set_type_class("module", extensions.module)
|
||||
self.optional_set_type_class("bpf_prog", extensions.bpf_prog)
|
||||
@@ -244,17 +245,17 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
):
|
||||
# task.files can be null
|
||||
if not task.files:
|
||||
return
|
||||
return None
|
||||
|
||||
fd_table = task.files.get_fds()
|
||||
if fd_table == 0:
|
||||
return
|
||||
return None
|
||||
|
||||
max_fds = task.files.get_max_fds()
|
||||
|
||||
# corruption check
|
||||
if max_fds > 500000:
|
||||
return
|
||||
return None
|
||||
|
||||
file_type = symbol_table + constants.BANG + "file"
|
||||
|
||||
@@ -379,7 +380,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
"""
|
||||
|
||||
if not addr:
|
||||
return
|
||||
return None
|
||||
|
||||
type_dec = vmlinux.get_type(type_name)
|
||||
member_offset = type_dec.relative_child_offset(member_name)
|
||||
|
||||
@@ -71,11 +71,11 @@ class module(generic.GenericIntelProcess):
|
||||
def _get_sect_count(self, grp):
|
||||
"""Try to determine the number of valid sections"""
|
||||
arr = self._context.object(
|
||||
self.get_symbol_table().name + constants.BANG + "array",
|
||||
self.get_symbol_table_name() + constants.BANG + "array",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=grp.attrs,
|
||||
subtype=self._context.symbol_space.get_type(
|
||||
self.get_symbol_table().name + constants.BANG + "pointer"
|
||||
self.get_symbol_table_name() + constants.BANG + "pointer"
|
||||
),
|
||||
count=25,
|
||||
)
|
||||
@@ -92,11 +92,11 @@ class module(generic.GenericIntelProcess):
|
||||
else:
|
||||
num_sects = self._get_sect_count(self.sect_attrs.grp)
|
||||
arr = self._context.object(
|
||||
self.get_symbol_table().name + constants.BANG + "array",
|
||||
self.get_symbol_table_name() + constants.BANG + "array",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=self.sect_attrs.attrs.vol.offset,
|
||||
subtype=self._context.symbol_space.get_type(
|
||||
self.get_symbol_table().name + constants.BANG + "module_sect_attr"
|
||||
self.get_symbol_table_name() + constants.BANG + "module_sect_attr"
|
||||
),
|
||||
count=num_sects,
|
||||
)
|
||||
@@ -104,41 +104,82 @@ class module(generic.GenericIntelProcess):
|
||||
for attr in arr:
|
||||
yield attr
|
||||
|
||||
def get_symbols(self):
|
||||
if symbols.symbol_table_is_64bit(self._context, self.get_symbol_table().name):
|
||||
prefix = "Elf64_"
|
||||
else:
|
||||
prefix = "Elf32_"
|
||||
def get_elf_table_name(self):
|
||||
elf_table_name = intermed.IntermediateSymbolTable.create(
|
||||
self.context,
|
||||
self.config_path,
|
||||
self._context,
|
||||
"elf_symbol_table",
|
||||
"linux",
|
||||
"elf",
|
||||
native_types=None,
|
||||
class_types=elf.class_types,
|
||||
)
|
||||
return elf_table_name
|
||||
|
||||
def get_symbols(self):
|
||||
"""Get symbols of the module
|
||||
|
||||
Yields:
|
||||
A symbol object
|
||||
"""
|
||||
|
||||
if not hasattr(self, "_elf_table_name"):
|
||||
self._elf_table_name = self.get_elf_table_name()
|
||||
if symbols.symbol_table_is_64bit(self._context, self.get_symbol_table_name()):
|
||||
prefix = "Elf64_"
|
||||
else:
|
||||
prefix = "Elf32_"
|
||||
syms = self._context.object(
|
||||
self.get_symbol_table().name + constants.BANG + "array",
|
||||
self.get_symbol_table_name() + constants.BANG + "array",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=self.section_symtab,
|
||||
subtype=self._context.symbol_space.get_type(
|
||||
elf_table_name + constants.BANG + prefix + "Sym"
|
||||
self._elf_table_name + constants.BANG + prefix + "Sym"
|
||||
),
|
||||
count=self.num_symtab + 1,
|
||||
)
|
||||
if self.section_strtab:
|
||||
for sym in syms:
|
||||
sym.set_cached_strtab(self.section_strtab)
|
||||
yield sym
|
||||
|
||||
def get_symbol(self, wanted_sym_name):
|
||||
"""Get value for a given symbol name"""
|
||||
def get_symbols_names_and_addresses(self) -> Tuple[str, int]:
|
||||
"""Get names and addresses for each symbol of the module
|
||||
|
||||
Yields:
|
||||
A tuple for each symbol containing the symbol name and its corresponding value
|
||||
"""
|
||||
|
||||
for sym in self.get_symbols():
|
||||
sym_name = sym.get_name()
|
||||
sym_addr = sym.st_value
|
||||
sym_arr = self._context.object(
|
||||
self.get_symbol_table_name() + constants.BANG + "array",
|
||||
layer_name=self.vol.native_layer_name,
|
||||
offset=self.section_strtab + sym.st_name,
|
||||
)
|
||||
try:
|
||||
sym_name = utility.array_to_string(
|
||||
sym_arr, 512
|
||||
) # 512 is the value of KSYM_NAME_LEN kernel constant
|
||||
except exceptions.InvalidAddressException:
|
||||
continue
|
||||
if sym_name != "":
|
||||
# Normalize sym.st_value offset, which is an address pointing to the symbol value
|
||||
mask = self._context.layers[self.vol.layer_name].address_mask
|
||||
sym_address = sym.st_value & mask
|
||||
yield (sym_name, sym_address)
|
||||
|
||||
def get_symbol(self, wanted_sym_name):
|
||||
"""Get symbol value for a given symbol name"""
|
||||
for sym_name, sym_address in self.get_symbols_names_and_addresses():
|
||||
if wanted_sym_name == sym_name:
|
||||
return sym_addr
|
||||
return sym_address
|
||||
|
||||
return None
|
||||
|
||||
def get_symbol_by_address(self, wanted_sym_address):
|
||||
"""Get symbol name for a given symbol address"""
|
||||
for sym_name, sym_address in self.get_symbols_names_and_addresses():
|
||||
if wanted_sym_address == sym_address:
|
||||
return sym_name
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -203,7 +244,7 @@ class task_struct(generic.GenericIntelProcess):
|
||||
) -> Generator[Tuple[int, int], None, None]:
|
||||
"""Returns a list of sections based on the memory manager's view of
|
||||
this task's virtual memory."""
|
||||
for vma in self.mm.get_mmap_iter():
|
||||
for vma in self.mm.get_vma_iter():
|
||||
start = int(vma.vm_start)
|
||||
end = int(vma.vm_end)
|
||||
|
||||
@@ -309,19 +350,30 @@ class maple_tree(objects.StructType):
|
||||
maple_tree_entry,
|
||||
parent,
|
||||
expected_maple_tree_depth,
|
||||
seen=set(),
|
||||
seen=None,
|
||||
current_depth=1,
|
||||
):
|
||||
"""Recursively parse Maple Tree Nodes and yield all non empty slots"""
|
||||
|
||||
# Create seen set if it does not exist, e.g. on the first call into this recursive function. This
|
||||
# must be None or an existing set of addresses for MTEs that have already been processed or that
|
||||
# should otherwise be ignored. If parsing from the root node for example this should be None on the
|
||||
# first call. If you needed to parse all nodes downwards from part of the tree this should still be
|
||||
# None. If however you wanted to parse from a node, but ignore some parts of the tree below it then
|
||||
# this could be populated with the addresses of the nodes you wish to ignore.
|
||||
|
||||
if seen == None:
|
||||
seen = set()
|
||||
|
||||
# protect against unlikely loop
|
||||
if maple_tree_entry in seen:
|
||||
vollog.warning(
|
||||
f"The mte {hex(maple_tree_entry)} has all ready been seen, no further results will be produced for this node."
|
||||
)
|
||||
return
|
||||
return None
|
||||
else:
|
||||
seen.add(maple_tree_entry)
|
||||
|
||||
# check if we have exceeded the expected depth of this maple tree.
|
||||
# e.g. when current_depth is larger than expected_maple_tree_depth there may be an issue.
|
||||
# it is normal that expected_maple_tree_depth is equal to current_depth.
|
||||
@@ -330,6 +382,7 @@ class maple_tree(objects.StructType):
|
||||
f"The depth for the maple tree at {hex(self.vol.offset)} is {expected_maple_tree_depth}, however when parsing the nodes "
|
||||
f"a depth of {current_depth} was reached. This is unexpected and may lead to incorrect results."
|
||||
)
|
||||
|
||||
# parse the mte to extract the pointer value, node type, and leaf status
|
||||
pointer = maple_tree_entry & ~(self.MAPLE_NODE_POINTER_MASK)
|
||||
node_type = (
|
||||
@@ -402,7 +455,7 @@ class mm_struct(objects.StructType):
|
||||
"get_mmap_iter called on mm_struct where no mmap member exists."
|
||||
)
|
||||
if not self.mmap:
|
||||
return
|
||||
return None
|
||||
yield self.mmap
|
||||
|
||||
seen = {self.mmap.vol.offset}
|
||||
@@ -578,7 +631,7 @@ class vm_area_struct(objects.StructType):
|
||||
return fname
|
||||
|
||||
# used by malfind
|
||||
def is_suspicious(self):
|
||||
def is_suspicious(self, proclayer=None):
|
||||
ret = False
|
||||
|
||||
flags_str = self.get_protection()
|
||||
@@ -587,6 +640,24 @@ class vm_area_struct(objects.StructType):
|
||||
ret = True
|
||||
elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0:
|
||||
ret = True
|
||||
elif proclayer and "x" in flags_str:
|
||||
for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT):
|
||||
try:
|
||||
if proclayer.is_dirty(i):
|
||||
vollog.warning(
|
||||
f"Found malicious (dirty+exec) page at {hex(i)} !"
|
||||
)
|
||||
# We do not attempt to find other dirty+exec pages once we have found one
|
||||
ret = True
|
||||
break
|
||||
except (
|
||||
exceptions.PagedInvalidAddressException,
|
||||
exceptions.InvalidAddressException,
|
||||
) as excp:
|
||||
vollog.debug(f"Unable to translate address {hex(i)} : {excp}")
|
||||
# Abort as it is likely that other addresses in the same range will also fail
|
||||
ret = False
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
@@ -705,7 +776,7 @@ class list_head(objects.StructType, collections.abc.Iterable):
|
||||
try:
|
||||
link = getattr(self, direction).dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
if not sentinel:
|
||||
yield self._context.object(
|
||||
symbol_type, layer, offset=self.vol.offset - relative_offset
|
||||
@@ -1114,7 +1185,7 @@ class vfsmount(objects.StructType):
|
||||
class kobject(objects.StructType):
|
||||
def reference_count(self):
|
||||
refcnt = self.kref.refcount
|
||||
if self.has_member("counter"):
|
||||
if refcnt.has_member("counter"):
|
||||
ret = refcnt.counter
|
||||
else:
|
||||
ret = refcnt.refs.counter
|
||||
@@ -1200,7 +1271,7 @@ class sock(objects.StructType):
|
||||
return self.sk_socket.get_inode()
|
||||
|
||||
def get_protocol(self):
|
||||
return
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
# Return the generic socket state
|
||||
@@ -1212,13 +1283,13 @@ class sock(objects.StructType):
|
||||
class unix_sock(objects.StructType):
|
||||
def get_name(self):
|
||||
if not self.addr:
|
||||
return
|
||||
return None
|
||||
sockaddr_un = self.addr.name.cast("sockaddr_un")
|
||||
saddr = str(utility.array_to_string(sockaddr_un.sun_path))
|
||||
return saddr
|
||||
|
||||
def get_protocol(self):
|
||||
return
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
"""Return a string representing the sock state."""
|
||||
@@ -1277,7 +1348,7 @@ class inet_sock(objects.StructType):
|
||||
elif hasattr(sk_common, "skc_dport"):
|
||||
dport_le = sk_common.skc_dport
|
||||
else:
|
||||
return
|
||||
return None
|
||||
return socket_module.htons(dport_le)
|
||||
|
||||
def get_src_addr(self):
|
||||
@@ -1295,7 +1366,7 @@ class inet_sock(objects.StructType):
|
||||
addr_size = 16
|
||||
saddr = self.pinet6.saddr
|
||||
else:
|
||||
return
|
||||
return None
|
||||
parent_layer = self._context.layers[self.vol.layer_name]
|
||||
try:
|
||||
addr_bytes = parent_layer.read(saddr.vol.offset, addr_size)
|
||||
@@ -1303,7 +1374,7 @@ class inet_sock(objects.StructType):
|
||||
vollog.debug(
|
||||
f"Unable to read socket src address from {saddr.vol.offset:#x}"
|
||||
)
|
||||
return
|
||||
return None
|
||||
return socket_module.inet_ntop(family, addr_bytes)
|
||||
|
||||
def get_dst_addr(self):
|
||||
@@ -1324,7 +1395,7 @@ class inet_sock(objects.StructType):
|
||||
daddr = sk_common.skc_v6_daddr
|
||||
addr_size = 16
|
||||
else:
|
||||
return
|
||||
return None
|
||||
parent_layer = self._context.layers[self.vol.layer_name]
|
||||
try:
|
||||
addr_bytes = parent_layer.read(daddr.vol.offset, addr_size)
|
||||
@@ -1332,7 +1403,7 @@ class inet_sock(objects.StructType):
|
||||
vollog.debug(
|
||||
f"Unable to read socket dst address from {daddr.vol.offset:#x}"
|
||||
)
|
||||
return
|
||||
return None
|
||||
return socket_module.inet_ntop(family, addr_bytes)
|
||||
|
||||
|
||||
@@ -1370,7 +1441,7 @@ class netlink_sock(objects.StructType):
|
||||
class vsock_sock(objects.StructType):
|
||||
def get_protocol(self):
|
||||
# The protocol should always be 0 for vsocks
|
||||
return
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
# Return the generic socket state
|
||||
@@ -1381,7 +1452,7 @@ class packet_sock(objects.StructType):
|
||||
def get_protocol(self):
|
||||
eth_proto = socket_module.htons(self.num)
|
||||
if eth_proto == 0:
|
||||
return
|
||||
return None
|
||||
elif eth_proto in ETH_PROTOCOLS:
|
||||
return ETH_PROTOCOLS[eth_proto]
|
||||
else:
|
||||
@@ -1407,7 +1478,7 @@ class bt_sock(objects.StructType):
|
||||
class xdp_sock(objects.StructType):
|
||||
def get_protocol(self):
|
||||
# The protocol should always be 0 for xdp_sock
|
||||
return
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
# xdp_sock.state is an enum
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
#
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import logging
|
||||
|
||||
from volatility3.framework import constants
|
||||
from volatility3.framework import objects, interfaces
|
||||
from volatility3.framework import objects, interfaces, exceptions
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class elf(objects.StructType):
|
||||
@@ -33,14 +36,23 @@ class elf(objects.StructType):
|
||||
layer_name = self.vol.layer_name
|
||||
symbol_table_name = self.get_symbol_table_name()
|
||||
# We read the MAGIC: (0x0 to 0x4) 0x7f 0x45 0x4c 0x46
|
||||
magic = self._context.object(
|
||||
symbol_table_name + constants.BANG + "unsigned long",
|
||||
layer_name=layer_name,
|
||||
offset=object_info.offset,
|
||||
)
|
||||
try:
|
||||
magic = self._context.object(
|
||||
symbol_table_name + constants.BANG + "unsigned long",
|
||||
layer_name=layer_name,
|
||||
offset=object_info.offset,
|
||||
)
|
||||
except (
|
||||
exceptions.PagedInvalidAddressException,
|
||||
exceptions.InvalidAddressException,
|
||||
) as excp:
|
||||
vollog.debug(
|
||||
f"Unable to check magic bytes for ELF file at offset {hex(object_info.offset)} in layer {layer_name}: {excp}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Check validity
|
||||
if magic != 0x464C457F:
|
||||
if magic != 0x464C457F: # e.g. ELF
|
||||
return None
|
||||
|
||||
# We need to read the EI_CLASS (0x4 offset)
|
||||
@@ -72,7 +84,10 @@ class elf(objects.StructType):
|
||||
"""
|
||||
Determine whether it is a valid object
|
||||
"""
|
||||
return self._type_prefix is not None and self._hdr is not None
|
||||
if hasattr(self, "_type_prefix") and hasattr(self, "_hdr"):
|
||||
return self._type_prefix is not None and self._hdr is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
def __getattr__(self, name):
|
||||
# Just redirect to the corresponding header
|
||||
@@ -171,7 +186,7 @@ class elf(objects.StructType):
|
||||
self._find_symbols()
|
||||
|
||||
if self._cached_symtab is None:
|
||||
return
|
||||
return None
|
||||
|
||||
symtab_arr = self._context.object(
|
||||
self.get_symbol_table_name() + constants.BANG + "array",
|
||||
|
||||
@@ -169,7 +169,7 @@ class MacUtilities(interfaces.configuration.VersionableInterface):
|
||||
try:
|
||||
table_addr = task.p_fd.fd_ofiles.dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
fds = objects.utility.array_of_pointers(
|
||||
table_addr, count=num_fds, subtype=file_type, context=context
|
||||
@@ -204,7 +204,7 @@ class MacUtilities(interfaces.configuration.VersionableInterface):
|
||||
try:
|
||||
current = queue.member(attr=list_head_member)
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
while current:
|
||||
if current.vol.offset in seen:
|
||||
|
||||
@@ -50,7 +50,7 @@ class proc(generic.GenericIntelProcess):
|
||||
task = self.get_task()
|
||||
current_map = task.map.hdr.links.next
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
seen: Set[int] = set()
|
||||
|
||||
@@ -138,13 +138,13 @@ class vm_map_object(objects.StructType):
|
||||
class vnode(objects.StructType):
|
||||
def _do_calc_path(self, ret, vnodeobj, vname):
|
||||
if vnodeobj is None:
|
||||
return
|
||||
return None
|
||||
|
||||
if vname:
|
||||
try:
|
||||
ret.append(utility.pointer_to_string(vname, 255))
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
if int(vnodeobj.v_flag) & 0x000001 != 0 and int(vnodeobj.v_mount) != 0:
|
||||
if int(vnodeobj.v_mount.mnt_vnodecovered) != 0:
|
||||
@@ -158,7 +158,7 @@ class vnode(objects.StructType):
|
||||
parent = vnodeobj.v_parent
|
||||
parent_name = parent.v_name
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
self._do_calc_path(ret, parent, parent_name)
|
||||
|
||||
@@ -502,7 +502,7 @@ class queue_entry(objects.StructType):
|
||||
|
||||
yielded = yielded + 1
|
||||
if yielded == max_size:
|
||||
return
|
||||
return None
|
||||
|
||||
n = (
|
||||
getattr(n.member(attr=member_name), attr)
|
||||
|
||||
@@ -91,7 +91,7 @@ class MMVAD_SHORT(objects.StructType):
|
||||
|
||||
if vad_address in visited:
|
||||
vollog.log(constants.LOGLEVEL_VVV, "VAD node already seen!")
|
||||
return
|
||||
return None
|
||||
|
||||
visited.add(vad_address)
|
||||
tag = self.get_tag()
|
||||
@@ -111,7 +111,7 @@ class MMVAD_SHORT(objects.StructType):
|
||||
constants.LOGLEVEL_VVV,
|
||||
f"Skipping VAD at {self.vol.offset} depth {depth} with tag {tag}",
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
if target:
|
||||
vad_object = self.cast(target)
|
||||
@@ -665,7 +665,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
|
||||
):
|
||||
yield entry
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
def init_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Generator for DLLs in the order that they were initialized"""
|
||||
@@ -678,7 +678,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
|
||||
):
|
||||
yield entry
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
def mem_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Generator for DLLs in the order that they appear in memory"""
|
||||
@@ -691,7 +691,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
|
||||
):
|
||||
yield entry
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
def get_handle_count(self):
|
||||
try:
|
||||
@@ -841,11 +841,11 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
|
||||
try:
|
||||
is_valid = trans_layer.is_valid(self.vol.offset)
|
||||
if not is_valid:
|
||||
return
|
||||
return None
|
||||
|
||||
link = getattr(self, direction).dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
if not sentinel:
|
||||
yield self._context.object(
|
||||
@@ -860,7 +860,7 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
|
||||
obj_offset = link.vol.offset - relative_offset
|
||||
|
||||
if not trans_layer.is_valid(obj_offset):
|
||||
return
|
||||
return None
|
||||
|
||||
obj = self._context.object(
|
||||
symbol_type,
|
||||
@@ -875,7 +875,7 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable):
|
||||
try:
|
||||
link = getattr(link, direction).dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]:
|
||||
return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name)
|
||||
@@ -905,10 +905,10 @@ class TOKEN(objects.StructType):
|
||||
sid = sid_and_attr.Sid.dereference().cast("_SID")
|
||||
# catch invalid pointers (UserAndGroupCount is too high)
|
||||
if sid is None:
|
||||
return
|
||||
return None
|
||||
# this mimics the windows API IsValidSid
|
||||
if sid.Revision & 0xF != 1 or sid.SubAuthorityCount > 15:
|
||||
return
|
||||
return None
|
||||
id_auth = ""
|
||||
for i in sid.IdentifierAuthority.Value:
|
||||
id_auth = i
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
from volatility3.framework import objects
|
||||
from volatility3.framework import objects, constants, exceptions
|
||||
|
||||
|
||||
class MFTEntry(objects.StructType):
|
||||
@@ -21,3 +21,36 @@ class MFTFileName(objects.StructType):
|
||||
"string", encoding="utf16", max_length=self.NameLength * 2, errors="replace"
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
class MFTAttribute(objects.StructType):
|
||||
"""This represents an MFT ATTRIBUTE"""
|
||||
|
||||
def get_resident_filename(self) -> str:
|
||||
# To get the resident name, we jump to relative name offset and read name length * 2 bytes of data
|
||||
try:
|
||||
name = self._context.object(
|
||||
self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=self.vol.offset + self.Attr_Header.NameOffset,
|
||||
max_length=self.Attr_Header.NameLength * 2,
|
||||
errors="replace",
|
||||
encoding="utf16",
|
||||
)
|
||||
return name
|
||||
except exceptions.InvalidAddressException:
|
||||
return None
|
||||
|
||||
def get_resident_filecontent(self) -> bytes:
|
||||
# To get the resident content, we jump to relative content offset and read name length * 2 bytes of data
|
||||
try:
|
||||
bytesobj = self._context.object(
|
||||
self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "bytes",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=self.vol.offset + self.Attr_Header.ContentOffset,
|
||||
native_layer_name=self.vol.native_layer_name,
|
||||
length=self.Attr_Header.ContentLength,
|
||||
)
|
||||
return bytesobj
|
||||
except exceptions.InvalidAddressException:
|
||||
return None
|
||||
|
||||
@@ -162,7 +162,7 @@ class CM_KEY_NODE(objects.StructType):
|
||||
try:
|
||||
signature = node.cast("string", max_length=2, encoding="latin-1")
|
||||
except (exceptions.InvalidAddressException, RegistryFormatException):
|
||||
return
|
||||
return None
|
||||
|
||||
listjump = None
|
||||
if signature == "ri":
|
||||
@@ -220,7 +220,7 @@ class CM_KEY_NODE(objects.StructType):
|
||||
yield node
|
||||
except (exceptions.InvalidAddressException, RegistryFormatException) as excp:
|
||||
vollog.debug(f"Invalid address in get_values iteration: {excp}")
|
||||
return
|
||||
return None
|
||||
|
||||
def get_name(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Gets the name for the current key node"""
|
||||
|
||||
@@ -110,7 +110,7 @@ class SERVICE_RECORD(objects.StructType):
|
||||
yield rec
|
||||
rec = rec.ServiceList.Blink.dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
|
||||
|
||||
class SERVICE_HEADER(objects.StructType):
|
||||
|
||||
@@ -230,21 +230,21 @@
|
||||
"offset": 0,
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"name": "mft!ATTR_HEADER"
|
||||
"name": "ATTR_HEADER"
|
||||
}
|
||||
},
|
||||
"Resident_Header": {
|
||||
"offset": 16,
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"name": "mft!RESIDENT_HEADER"
|
||||
"name": "RESIDENT_HEADER"
|
||||
}
|
||||
},
|
||||
"Attr_Data": {
|
||||
"offset": 24,
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"name": "mft!ATTR_HEADER"
|
||||
"name": "ATTR_HEADER"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -300,10 +300,24 @@
|
||||
"kind": "base",
|
||||
"name": "unsigned short"
|
||||
}
|
||||
},
|
||||
"ContentLength": {
|
||||
"offset": 16,
|
||||
"type": {
|
||||
"kind": "base",
|
||||
"name": "unsigned int"
|
||||
}
|
||||
},
|
||||
"ContentOffset": {
|
||||
"offset": 20,
|
||||
"type": {
|
||||
"kind": "base",
|
||||
"name": "unsigned short"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": "struct",
|
||||
"size": 16
|
||||
"size": 24
|
||||
},"RESIDENT_HEADER": {
|
||||
"fields": {
|
||||
"AttrSize": {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import lzma
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from pathlib import PureWindowsPath
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
from urllib import parse, request
|
||||
|
||||
@@ -226,13 +226,12 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
|
||||
return None
|
||||
|
||||
pdb_name = debug_entry.PdbFileName.decode("utf-8").strip("\x00")
|
||||
|
||||
# Let pathlib do the filename extraction. This will likely always be a Windows path though.
|
||||
pdb_name = PureWindowsPath(pdb_name).name
|
||||
|
||||
age = debug_entry.Age
|
||||
guid = "{:08x}{:04x}{:04x}{}".format(
|
||||
debug_entry.Signature_Data1,
|
||||
debug_entry.Signature_Data2,
|
||||
debug_entry.Signature_Data3,
|
||||
binascii.hexlify(debug_entry.Signature_Data4).decode("utf-8"),
|
||||
)
|
||||
guid = debug_entry.Signature_String[:32] # Removes the Age from the GUID
|
||||
return guid, age, pdb_name
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user