mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge pull request #1935 from volatilityfoundation/release/v2.27.0
Release/v2.27.0
This commit is contained in:
@@ -28,7 +28,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller
|
||||
pip install -e .[full,cloud]
|
||||
pip install -e .[full,cloud,arrow]
|
||||
|
||||
- name: Pyinstall executable
|
||||
run: |
|
||||
|
||||
@@ -66,13 +66,17 @@ pip install -e ".[dev]"
|
||||
Symbol table packs for the various operating systems are available for download at:
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip>
|
||||
|
||||
The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at:
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA256SUMS>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA1SUMS>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/MD5SUMS>
|
||||
|
||||
Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file).
|
||||
@@ -92,7 +96,7 @@ The latest generated copy of the documentation can be found at: <https://volatil
|
||||
|
||||
## Licensing and Copyright
|
||||
|
||||
Copyright (C) 2007-2025 Volatility Foundation
|
||||
Copyright (C) 2007-2026 Volatility Foundation
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BannerCacheGenerator:
|
||||
|
||||
def __init__(self, path: str, url_prefix: str):
|
||||
self._path = path
|
||||
self._url_prefix = url_prefix
|
||||
@@ -79,7 +78,6 @@ class BannerCacheGenerator:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--path", default=os.path.dirname(__file__))
|
||||
parser.add_argument(
|
||||
|
||||
@@ -208,7 +208,6 @@ class Volatility3PyPyTest(VolatilityTest):
|
||||
|
||||
|
||||
class VolatilityTester:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
images: List[VolatilityImage],
|
||||
|
||||
@@ -22,7 +22,6 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
class PDBRetreiver:
|
||||
|
||||
def retreive_pdb(self, guid: str, file_name: str) -> Optional[str]:
|
||||
logger.info("Download PDB file...")
|
||||
file_name = ".".join(file_name.split(".")[:-1] + ["pdb"])
|
||||
|
||||
@@ -13,7 +13,6 @@ DWARF2JSON = "./dwarf2json"
|
||||
|
||||
|
||||
class Downloader:
|
||||
|
||||
def __init__(self, url_lists: List[List[str]]) -> None:
|
||||
self.url_lists = url_lists
|
||||
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# This file is Copyright 2026 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
#
|
||||
@@ -16,11 +16,10 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from importlib.util import find_spec
|
||||
|
||||
import sphinx.ext.apidoc
|
||||
|
||||
from importlib.util import find_spec
|
||||
|
||||
|
||||
def setup(app):
|
||||
volatility_directory = os.path.abspath(
|
||||
@@ -167,7 +166,7 @@ master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = "Volatility 3"
|
||||
copyright = "2012-2025, Volatility Foundation"
|
||||
copyright = "2012-2026, Volatility Foundation"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
||||
+3
-1
@@ -42,6 +42,8 @@ dev = [
|
||||
"types-jsonschema>=4.23.0,<5",
|
||||
]
|
||||
|
||||
arrow = ["pyarrow>=17.0.0"]
|
||||
|
||||
test = [
|
||||
"volatility3[dev]",
|
||||
"pytest>=8.3.3,<9",
|
||||
@@ -52,7 +54,7 @@ test = [
|
||||
docs = [
|
||||
"volatility3[dev]",
|
||||
"sphinx>=4.0.0,<9",
|
||||
"sphinx-autodoc-typehints>=3.0.0,<4",
|
||||
"sphinx-autodoc-typehints>=3.0.0,<4; python_version >= '3.11'",
|
||||
"sphinx-rtd-theme>=3.0.1,<4",
|
||||
]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from test import WindowsSamples, test_volatility
|
||||
|
||||
|
||||
@@ -437,7 +438,7 @@ class TestWindowsVadyarascan:
|
||||
class TestWindowsAmcache:
|
||||
def test_windows_generic_amcache(self, volatility, python, image):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"windows.amcache.Amcache",
|
||||
"windows.registry.amcache.Amcache",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
@@ -492,7 +493,7 @@ class TestWindowsBigPools:
|
||||
# class TestWindowsCachedump:
|
||||
# def test_windows_generic_cachedump(self, volatility, python, image):
|
||||
# rc, out, _err = test_volatility.runvol_plugin(
|
||||
# "windows.cachedump.Cachedump",
|
||||
# "windows.registry.cachedump.Cachedump",
|
||||
# image,
|
||||
# volatility,
|
||||
# python,
|
||||
@@ -820,7 +821,7 @@ class TestWindowsLsadump:
|
||||
def test_windows_specific_lsadump(self, volatility, python):
|
||||
image = WindowsSamples.WINDOWSXP_GENERIC.value.path
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"windows.lsadump.Lsadump",
|
||||
"windows.registry.lsadump.Lsadump",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import io
|
||||
import pytest
|
||||
from abc import ABC, abstractmethod
|
||||
from test import test_volatility
|
||||
|
||||
HAS_PYARROW = False
|
||||
try:
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import pyarrow.compute as pc
|
||||
|
||||
HAS_PYARROW = True
|
||||
except ImportError:
|
||||
# The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed")
|
||||
class TestArrowRendererBase(ABC):
|
||||
"""Base class for testing Arrow-based renderers.
|
||||
|
||||
Re-implements Windows and Linux plugin tests using PyArrow operations
|
||||
instead of text-based assertions.
|
||||
"""
|
||||
|
||||
renderer_format = None # Override in subclasses
|
||||
|
||||
@abstractmethod
|
||||
def _get_table_from_output(self, output_bytes) -> "pa.Table":
|
||||
"""Parse output bytes into Arrow table. Override in subclasses."""
|
||||
|
||||
def test_windows_generic_pslist(self, volatility, python, image):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"windows.pslist.PsList",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
globalargs=("-r", self.renderer_format),
|
||||
)
|
||||
assert rc == 0
|
||||
|
||||
table = self._get_table_from_output(out)
|
||||
assert table.num_rows > 10
|
||||
|
||||
assert (
|
||||
table.filter(
|
||||
pc.match_substring(
|
||||
pc.utf8_lower(table.column("ImageFileName")), "system"
|
||||
)
|
||||
).num_rows
|
||||
> 0
|
||||
)
|
||||
assert (
|
||||
table.filter(
|
||||
pc.match_substring(
|
||||
pc.utf8_lower(table.column("ImageFileName")), "csrss.exe"
|
||||
)
|
||||
).num_rows
|
||||
> 0
|
||||
)
|
||||
assert (
|
||||
table.filter(
|
||||
pc.match_substring(
|
||||
pc.utf8_lower(table.column("ImageFileName")), "svchost.exe"
|
||||
)
|
||||
).num_rows
|
||||
> 0
|
||||
)
|
||||
assert (
|
||||
table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows
|
||||
)
|
||||
|
||||
def test_linux_generic_pslist(self, volatility, python, image):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"linux.pslist.PsList",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
globalargs=("-r", self.renderer_format),
|
||||
)
|
||||
assert rc == 0
|
||||
|
||||
table = self._get_table_from_output(out)
|
||||
assert table.num_rows > 10
|
||||
|
||||
init_rows = table.filter(
|
||||
pc.match_substring(pc.utf8_lower(table.column("COMM")), "init")
|
||||
)
|
||||
systemd_rows = table.filter(
|
||||
pc.match_substring(pc.utf8_lower(table.column("COMM")), "systemd")
|
||||
)
|
||||
assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0)
|
||||
|
||||
assert (
|
||||
table.filter(
|
||||
pc.match_substring(pc.utf8_lower(table.column("COMM")), "watchdog")
|
||||
).num_rows
|
||||
> 0
|
||||
)
|
||||
assert (
|
||||
table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows
|
||||
)
|
||||
|
||||
def test_windows_generic_handles(self, volatility, python, image):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"windows.handles.Handles",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
globalargs=("-r", self.renderer_format),
|
||||
pluginargs=("--pid", "4"),
|
||||
)
|
||||
assert rc == 0
|
||||
|
||||
table = self._get_table_from_output(out)
|
||||
assert table.num_rows > 500
|
||||
assert (
|
||||
table.filter(
|
||||
pc.match_substring(
|
||||
pc.utf8_lower(table.column("Name")), "machine\\system"
|
||||
)
|
||||
).num_rows
|
||||
> 0
|
||||
)
|
||||
|
||||
def test_linux_generic_lsof(self, volatility, python, image):
|
||||
rc, out, _err = test_volatility.runvol_plugin(
|
||||
"linux.lsof.Lsof",
|
||||
image,
|
||||
volatility,
|
||||
python,
|
||||
globalargs=("-r", self.renderer_format),
|
||||
)
|
||||
assert rc == 0
|
||||
|
||||
table = self._get_table_from_output(out)
|
||||
assert table.num_rows > 35
|
||||
|
||||
|
||||
class TestParquetRenderer(TestArrowRendererBase):
|
||||
renderer_format = "parquet"
|
||||
|
||||
def _get_table_from_output(self, output_bytes):
|
||||
return pq.read_table(io.BytesIO(output_bytes))
|
||||
|
||||
|
||||
class TestArrowRenderer(TestArrowRendererBase):
|
||||
renderer_format = "arrow"
|
||||
|
||||
def _get_table_from_output(self, output_bytes):
|
||||
return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all()
|
||||
@@ -82,7 +82,6 @@ class CodeViolation(metaclass=abc.ABCMeta):
|
||||
|
||||
|
||||
class UnrequiredVersionableUsage(CodeViolation):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module: types.ModuleType,
|
||||
@@ -107,7 +106,6 @@ class UnrequiredVersionableUsage(CodeViolation):
|
||||
|
||||
|
||||
class DirectVolatilityImportUsage(CodeViolation):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module: types.ModuleType,
|
||||
@@ -174,8 +172,11 @@ class ModuleVisitor(NodeVisitor):
|
||||
"""
|
||||
if (
|
||||
node.module
|
||||
and node.module.startswith("volatility3.") # Give a pass to volatility3 module
|
||||
and node.module != "volatility3.framework.constants._version" # make an exception for this
|
||||
and node.module.startswith(
|
||||
"volatility3."
|
||||
) # Give a pass to volatility3 module
|
||||
and node.module
|
||||
!= "volatility3.framework.constants._version" # make an exception for this
|
||||
):
|
||||
for name in node.names:
|
||||
try:
|
||||
@@ -204,7 +205,6 @@ class ModuleVisitor(NodeVisitor):
|
||||
def enter_ImportFrom(self, node: ast.ImportFrom):
|
||||
self._check_vol3_import_from(node)
|
||||
|
||||
|
||||
def enter_ClassDef(self, node: ast.ClassDef) -> Any:
|
||||
logger.debug("Entering class %s", node.name)
|
||||
clazz = None
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""Volatility 3 - An open-source memory forensics framework"""
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from importlib import abc
|
||||
|
||||
+31
-24
@@ -10,6 +10,7 @@ User interfaces make use of the framework to:
|
||||
* run the plugin
|
||||
* display the results
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import io
|
||||
@@ -106,13 +107,6 @@ class CommandLine:
|
||||
|
||||
volatility3.framework.require_interface_version(2, 0, 0)
|
||||
|
||||
renderers = dict(
|
||||
[
|
||||
(x.name.lower(), x)
|
||||
for x in framework.class_subclasses(text_renderer.CLIRenderer)
|
||||
]
|
||||
)
|
||||
|
||||
# Load up system defaults
|
||||
delayed_logs, default_config = self.load_system_defaults("vol.json")
|
||||
|
||||
@@ -193,14 +187,6 @@ class CommandLine:
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--renderer",
|
||||
metavar="RENDERER",
|
||||
help=f"Determines how to render the output ({', '.join(list(renderers))})",
|
||||
default="quick",
|
||||
choices=list(renderers),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
@@ -270,11 +256,6 @@ class CommandLine:
|
||||
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
|
||||
partial_args, _ = parser.parse_known_args(known_args)
|
||||
|
||||
banner_output = sys.stdout
|
||||
if renderers[partial_args.renderer].structured_output:
|
||||
banner_output = sys.stderr
|
||||
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")
|
||||
|
||||
### Start up logging
|
||||
if partial_args.log:
|
||||
file_logger = logging.FileHandler(partial_args.log)
|
||||
@@ -346,6 +327,24 @@ class CommandLine:
|
||||
|
||||
plugin_list = framework.list_plugins()
|
||||
|
||||
# Discover renderers after plugin directories are loaded
|
||||
# This allows custom renderers to be found in plugin directories
|
||||
renderers = dict(
|
||||
[
|
||||
(x.name.lower(), x)
|
||||
for x in framework.class_subclasses(text_renderer.CLIRenderer)
|
||||
]
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--renderer",
|
||||
metavar="RENDERER",
|
||||
help=f"Determines how to render the output ({', '.join(list(renderers))})",
|
||||
default="quick",
|
||||
choices=list(renderers),
|
||||
)
|
||||
|
||||
seen_automagics = set()
|
||||
chosen_configurables_list = {}
|
||||
for amagic in automagics:
|
||||
@@ -392,6 +391,13 @@ class CommandLine:
|
||||
# before all the plugins have been added
|
||||
argcomplete.autocomplete(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Display banner - redirect to stderr if using structured output
|
||||
banner_output = sys.stdout
|
||||
if renderers[args.renderer].structured_output:
|
||||
banner_output = sys.stderr
|
||||
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")
|
||||
|
||||
if args.plugin is None:
|
||||
parser.error(
|
||||
f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options"
|
||||
@@ -453,8 +459,9 @@ class CommandLine:
|
||||
raise ValueError(
|
||||
"Invalid extension (extensions must be of the format \"conf.path.value='value'\")"
|
||||
)
|
||||
address, value = extension[: extension.find("=")], json.loads(
|
||||
extension[extension.find("=") + 1 :]
|
||||
address, value = (
|
||||
extension[: extension.find("=")],
|
||||
json.loads(extension[extension.find("=") + 1 :]),
|
||||
)
|
||||
ctx.config[address] = value
|
||||
|
||||
@@ -569,7 +576,7 @@ class CommandLine:
|
||||
delayed_logs.append(
|
||||
(
|
||||
logging.DEBUG,
|
||||
f"Loaded configuration: {json.dumps(result, indent = 2, sort_keys = True)}",
|
||||
f"Loaded configuration: {json.dumps(result, indent=2, sort_keys=True)}",
|
||||
)
|
||||
)
|
||||
return delayed_logs, result
|
||||
@@ -758,7 +765,7 @@ class CommandLine:
|
||||
constants.LOGLEVEL_VVVV,
|
||||
]
|
||||
):
|
||||
logging.addLevelName(level_value, f"DETAIL {level+1}")
|
||||
logging.addLevelName(level_value, f"DETAIL {level + 1}")
|
||||
|
||||
def file_handler_class_factory(self, direct=True):
|
||||
output_dir = self.output_dir
|
||||
|
||||
@@ -278,7 +278,6 @@ class CLIRenderer(interfaces.renderers.Renderer):
|
||||
|
||||
|
||||
class QuickTextRenderer(CLIRenderer):
|
||||
|
||||
name = "quick"
|
||||
|
||||
def get_render_options(self):
|
||||
@@ -348,7 +347,6 @@ class NoneRenderer(CLIRenderer):
|
||||
|
||||
|
||||
class CSVRenderer(CLIRenderer):
|
||||
|
||||
name = "csv"
|
||||
structured_output = True
|
||||
|
||||
|
||||
@@ -344,8 +344,9 @@ class VolShell(cli.CommandLine):
|
||||
raise ValueError(
|
||||
"Invalid extension (extensions must be of the format \"conf.path.value='value'\")"
|
||||
)
|
||||
address, value = extension[: extension.find("=")], json.loads(
|
||||
extension[extension.find("=") + 1 :]
|
||||
address, value = (
|
||||
extension[: extension.find("=")],
|
||||
json.loads(extension[extension.find("=") + 1 :]),
|
||||
)
|
||||
ctx.config[address] = value
|
||||
|
||||
|
||||
@@ -469,7 +469,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
and dereference_count < MAX_DEREFERENCE_COUNT
|
||||
):
|
||||
# before defreerencing the pointer, show it's information
|
||||
print(f'{" " * dereference_count}{self._display_simple_type(volobject)}')
|
||||
print(f"{' ' * dereference_count}{self._display_simple_type(volobject)}")
|
||||
|
||||
# check that we can follow the pointer before dereferencing and do not
|
||||
# attempt to follow null pointers.
|
||||
@@ -486,7 +486,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
|
||||
if hasattr(volobject.vol, "members"):
|
||||
# display the header for this object, if the original object was just a type string, display the type information
|
||||
struct_header = f'{" " * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)'
|
||||
struct_header = f"{' ' * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)"
|
||||
if isinstance(object, str) and offset is None:
|
||||
suffix = ":"
|
||||
else:
|
||||
@@ -523,7 +523,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
len_typename = len(member_type_name)
|
||||
if len(member_type_name) > MAX_TYPENAME_DISPLAY_LENGTH:
|
||||
len_typename = MAX_TYPENAME_DISPLAY_LENGTH
|
||||
member_type_name = f"{member_type_name[:len_typename - 3]}..."
|
||||
member_type_name = f"{member_type_name[: len_typename - 3]}..."
|
||||
|
||||
if isinstance(volobject, interfaces.objects.ObjectInterface):
|
||||
# We're an instance, so also display the data
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""Volatility 3 framework."""
|
||||
|
||||
# Check the python version to ensure it's suitable
|
||||
import glob
|
||||
import sys
|
||||
|
||||
@@ -7,6 +7,7 @@ from loaded PE files.
|
||||
This module contains a standalone scanner, and also a :class:`~volatility3.framework.interfaces.layers.ScannerInterface`
|
||||
based scanner for use within the framework by calling :func:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan`.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
@@ -449,7 +450,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
|
||||
while (kernel_base + 0x2000000) > kernel_hint:
|
||||
for i in range(0, 0x200000, 0x1000):
|
||||
valid_kernel = self.check_kernel_offset(
|
||||
context, vlayer, kernel_base, progress_callback
|
||||
context, vlayer, kernel_base + i, progress_callback
|
||||
)
|
||||
if valid_kernel:
|
||||
return valid_kernel
|
||||
|
||||
@@ -153,8 +153,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
|
||||
constructor(context, config_path, requirement)
|
||||
|
||||
# Stash the changed config items
|
||||
self._cached = context.config.get(path, None), context.config.branch(
|
||||
path
|
||||
self._cached = (
|
||||
context.config.get(path, None),
|
||||
context.config.branch(path),
|
||||
)
|
||||
vollog.debug(
|
||||
f"physical_layer maximum_address: {physical_layer.maximum_address}"
|
||||
|
||||
@@ -26,6 +26,7 @@ The self-referential indices for older versions of windows are listed below:
|
||||
| x64 | 0x1ED |
|
||||
+--------------+-------+
|
||||
"""
|
||||
|
||||
import logging
|
||||
import struct
|
||||
from typing import Generator, Iterable, List, Optional, Tuple, Type
|
||||
|
||||
@@ -8,6 +8,7 @@ These requirement types allow plugins to request simple information
|
||||
types (such as strings, integers, etc) as well as indicating what they
|
||||
expect to be in the context (such as particular layers or symboltables).
|
||||
"""
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# We use the SemVer 2.0.0 versioning scheme
|
||||
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
|
||||
VERSION_MINOR = 26 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 2 # Number of changes that do not change the interface
|
||||
VERSION_MINOR = 27 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 0 # Number of changes that do not change the interface
|
||||
VERSION_SUFFIX = ""
|
||||
|
||||
PACKAGE_VERSION = (
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
Linux-specific values that aren't found in debug symbols
|
||||
"""
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ This has been made an object to allow quick swapping and changing of
|
||||
contexts, to allow a plugin to act on multiple different contexts
|
||||
without them interfering with each other.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
@@ -79,7 +79,7 @@ def deprecated_method(
|
||||
"This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.",
|
||||
)
|
||||
|
||||
deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated and will be removed in the first release after {removal_date}, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}"
|
||||
deprecation_msg = f'Method "{deprecated_func.__module__ + "." + deprecated_func.__qualname__}" is deprecated and will be removed in the first release after {removal_date}, use "{replacement.__module__ + "." + replacement.__qualname__}" instead. {additional_information}'
|
||||
warnings.warn(deprecation_msg, FutureWarning)
|
||||
# Return the wrapped function with its original arguments
|
||||
return deprecated_func(*args, **kwargs)
|
||||
|
||||
@@ -8,6 +8,7 @@ space or symbol tables, and by layers when an address is invalid. The
|
||||
:class:`PagedInvalidAddressException` contains information about the
|
||||
size of the invalid page.
|
||||
"""
|
||||
|
||||
from typing import Callable, Dict, Optional, Tuple
|
||||
|
||||
from volatility3.framework import interfaces
|
||||
@@ -161,4 +162,4 @@ class VersionMismatchException(VolatilityException):
|
||||
self.failure_reason = failure_reason
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet."
|
||||
return f"{self.source_component.__module__ + '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__ + '.' + self.target_component.__name__} {self.target_component.version} unmet."
|
||||
|
||||
@@ -7,6 +7,7 @@ runs.
|
||||
Automagic objects attempt to automatically fill configuration values
|
||||
that a user has not filled.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from abc import ABCMeta
|
||||
from typing import Any, List, Optional, Tuple, Type, Union
|
||||
|
||||
@@ -11,6 +11,7 @@ convenience functions, most notably the object constructor function,
|
||||
`object`, which will construct a symbol on a layer at a particular
|
||||
offset.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import copy
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
One layer may combine other layers, map data based on the data itself,
|
||||
or map a procedure (such as decryption) across another layer of data.
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#
|
||||
"""Objects are the core of volatility, and provide pythonic access to
|
||||
interpreted values of data from a layer."""
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Any, Dict, List, Mapping, NamedTuple, Optional
|
||||
from typing import Any, Dict, List, Mapping, Optional
|
||||
|
||||
from volatility3.framework import constants, interfaces
|
||||
|
||||
@@ -52,7 +54,8 @@ class ReadOnlyMapping(collections.abc.Mapping):
|
||||
return dict(self) == dict(other)
|
||||
|
||||
|
||||
class ObjectInformation(NamedTuple):
|
||||
@dataclasses.dataclass
|
||||
class ObjectInformation:
|
||||
"""Contains common information useful/pertinent only to an individual
|
||||
object (like an instance)
|
||||
|
||||
@@ -71,12 +74,12 @@ class ObjectInformation(NamedTuple):
|
||||
size: Optional[int] = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self._fields:
|
||||
if key in self:
|
||||
return getattr(self, key)
|
||||
raise KeyError(f"NamedTuple does not have a key {key}")
|
||||
raise KeyError(f"No {key} present in ObjectInformation")
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._fields
|
||||
return key in [field.name for field in dataclasses.fields(self)]
|
||||
|
||||
|
||||
class ObjectInterface(metaclass=abc.ABCMeta):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""Symbols provide structural information about a set of bytes."""
|
||||
|
||||
import bisect
|
||||
import collections.abc
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
The user of the file doesn't have to worry about the compression,
|
||||
but random access is not allowed."""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
|
||||
@@ -2,7 +2,4 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
"""Codecs used for encoding or decoding data should live here
|
||||
|
||||
|
||||
"""
|
||||
"""Codecs used for encoding or decoding data should live here"""
|
||||
|
||||
@@ -315,7 +315,13 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
):
|
||||
# The block isn't contiguous
|
||||
if stashed_offset is not None:
|
||||
yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer
|
||||
yield (
|
||||
stashed_offset,
|
||||
stashed_size,
|
||||
stashed_mapped_offset,
|
||||
stashed_mapped_size,
|
||||
stashed_map_layer,
|
||||
)
|
||||
# Update all the stashed values after output
|
||||
stashed_offset = offset
|
||||
stashed_mapped_offset = mapped_offset
|
||||
@@ -334,7 +340,13 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
and stashed_mapped_size is not None
|
||||
and stashed_map_layer is not None
|
||||
):
|
||||
yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer
|
||||
yield (
|
||||
stashed_offset,
|
||||
stashed_size,
|
||||
stashed_mapped_offset,
|
||||
stashed_mapped_size,
|
||||
stashed_map_layer,
|
||||
)
|
||||
|
||||
def _mapping(
|
||||
self, offset: int, length: int, ignore_errors: bool = False
|
||||
|
||||
@@ -234,9 +234,13 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
|
||||
layer_name=self.name, invalid_address=offset + returned
|
||||
)
|
||||
else:
|
||||
yield offset + returned, chunk_size, (
|
||||
self._pages[page] * page_size
|
||||
) + page_position, chunk_size, self._base_layer
|
||||
yield (
|
||||
offset + returned,
|
||||
chunk_size,
|
||||
(self._pages[page] * page_size) + page_position,
|
||||
chunk_size,
|
||||
self._base_layer,
|
||||
)
|
||||
returned += chunk_size
|
||||
length -= chunk_size
|
||||
|
||||
|
||||
@@ -305,8 +305,9 @@ class JarHandler(VolatilityHandler):
|
||||
def default_open(req: urllib.request.Request) -> Optional[Any]:
|
||||
"""Handles the request if it's the jar scheme."""
|
||||
if req.type == "jar":
|
||||
subscheme, remainder = req.full_url.split(":")[1], ":".join(
|
||||
req.full_url.split(":")[2:]
|
||||
subscheme, remainder = (
|
||||
req.full_url.split(":")[1],
|
||||
":".join(req.full_url.split(":")[2:]),
|
||||
)
|
||||
if subscheme != "file":
|
||||
vollog.log(
|
||||
|
||||
@@ -129,7 +129,13 @@ class NonLinearlySegmentedLayer(
|
||||
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
|
||||
yield (
|
||||
logical_offset,
|
||||
chunk_size,
|
||||
mapped_offset,
|
||||
mapped_size,
|
||||
self._base_layer,
|
||||
)
|
||||
current_offset += chunk_size
|
||||
# Terminate if we've gone (or reached) our required limit
|
||||
if current_offset >= offset + length:
|
||||
|
||||
@@ -65,10 +65,10 @@ class VmwareLayer(segmented.SegmentedLayer):
|
||||
data = meta_layer.read(0, header_size)
|
||||
magic, unknown, groupCount = struct.unpack(self.header_structure, data)
|
||||
if magic not in [
|
||||
b"\xD0\xBE\xD2\xBE",
|
||||
b"\xD1\xBA\xD1\xBA",
|
||||
b"\xD2\xBE\xD2\xBE",
|
||||
b"\xD3\xBE\xD3\xBE",
|
||||
b"\xd0\xbe\xd2\xbe",
|
||||
b"\xd1\xba\xd1\xba",
|
||||
b"\xd2\xbe\xd2\xbe",
|
||||
b"\xd3\xbe\xd3\xbe",
|
||||
]:
|
||||
raise VmwareFormatException(
|
||||
self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}"
|
||||
|
||||
@@ -60,8 +60,9 @@ class Banners(interfaces.plugins.PluginInterface):
|
||||
not in b" #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"
|
||||
]
|
||||
if not failed:
|
||||
yield format_hints.Hex(offset), str(
|
||||
data, encoding="latin-1", errors="?"
|
||||
yield (
|
||||
format_hints.Hex(offset),
|
||||
str(data, encoding="latin-1", errors="?"),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -72,9 +72,12 @@ class IsfInfo(plugins.PluginInterface):
|
||||
for extension in constants.ISF_EXTENSIONS:
|
||||
# By ending with an extension (and therefore, not /), we should not return any directories
|
||||
if name.endswith(extension):
|
||||
yield "jar:file:" + str(
|
||||
pathlib.Path(base_name)
|
||||
) + "!" + name
|
||||
yield (
|
||||
"jar:file:"
|
||||
+ str(pathlib.Path(base_name))
|
||||
+ "!"
|
||||
+ name
|
||||
)
|
||||
|
||||
else:
|
||||
for extension in constants.ISF_EXTENSIONS:
|
||||
|
||||
@@ -245,7 +245,6 @@ You can try using ffmpeg to decode the raw buffer. Example usage:
|
||||
return fb
|
||||
|
||||
def _generator(self):
|
||||
|
||||
if not has_pil:
|
||||
vollog.error(
|
||||
"PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml."
|
||||
|
||||
@@ -47,7 +47,17 @@ class Addr(plugins.PluginInterface):
|
||||
prefix_len = in_ifaddr.get_prefix_len()
|
||||
scope_type = in_ifaddr.get_scope_type()
|
||||
ip_addr = in_ifaddr.get_address()
|
||||
yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type, operational_state
|
||||
yield (
|
||||
net_ns_id,
|
||||
iface_ifindex,
|
||||
iface_name,
|
||||
mac_addr,
|
||||
promisc,
|
||||
ip_addr,
|
||||
prefix_len,
|
||||
scope_type,
|
||||
operational_state,
|
||||
)
|
||||
|
||||
# Interface IPv6 Addresses
|
||||
inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev")
|
||||
@@ -55,7 +65,17 @@ class Addr(plugins.PluginInterface):
|
||||
prefix_len = inet6_ifaddr.get_prefix_len()
|
||||
scope_type = inet6_ifaddr.get_scope_type()
|
||||
ip6_addr = inet6_ifaddr.get_address()
|
||||
yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type, operational_state
|
||||
yield (
|
||||
net_ns_id,
|
||||
iface_ifindex,
|
||||
iface_name,
|
||||
mac_addr,
|
||||
promisc,
|
||||
ip6_addr,
|
||||
prefix_len,
|
||||
scope_type,
|
||||
operational_state,
|
||||
)
|
||||
|
||||
def _enumerate_net_namespace_list(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
@@ -82,16 +102,19 @@ class Addr(plugins.PluginInterface):
|
||||
scope_type,
|
||||
operational_state,
|
||||
) in self._gather_net_dev_info(net_dev):
|
||||
yield 0, (
|
||||
net_ns_id or renderers.NotAvailableValue(),
|
||||
iface_ifindex,
|
||||
iface_name,
|
||||
mac_addr,
|
||||
promisc,
|
||||
ip6_addr,
|
||||
prefix_len,
|
||||
scope_type,
|
||||
operational_state,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
net_ns_id or renderers.NotAvailableValue(),
|
||||
iface_ifindex,
|
||||
iface_name,
|
||||
mac_addr,
|
||||
promisc,
|
||||
ip6_addr,
|
||||
prefix_len,
|
||||
scope_type,
|
||||
operational_state,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
@@ -150,7 +173,16 @@ class Link(plugins.PluginInterface):
|
||||
]
|
||||
flags_str = ",".join(flags_list)
|
||||
|
||||
yield net_ns_id or renderers.NotAvailableValue(), iface_name, mac_addr, operational_state, mtu, qdisc_name or renderers.NotAvailableValue(), qlen, flags_str
|
||||
yield (
|
||||
net_ns_id or renderers.NotAvailableValue(),
|
||||
iface_name,
|
||||
mac_addr,
|
||||
operational_state,
|
||||
mtu,
|
||||
qdisc_name or renderers.NotAvailableValue(),
|
||||
qlen,
|
||||
flags_str,
|
||||
)
|
||||
|
||||
def _generator(self):
|
||||
vmlinux = self.context.modules[self.config["kernel"]]
|
||||
|
||||
@@ -551,12 +551,15 @@ class Kmsg(interfaces.plugins.PluginInterface):
|
||||
for facility, level, timestamp, caller, line in ABCKmsg.run_all(
|
||||
context=self.context, config=self.config
|
||||
):
|
||||
yield 0, (
|
||||
facility,
|
||||
level,
|
||||
timestamp,
|
||||
caller or renderers.NotAvailableValue(),
|
||||
line,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
facility,
|
||||
level,
|
||||
timestamp,
|
||||
caller or renderers.NotAvailableValue(),
|
||||
line,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"""A module containing a plugin that lists loaded kernel modules."""
|
||||
|
||||
import logging
|
||||
from typing import List, Iterable
|
||||
from typing import Iterable, List
|
||||
|
||||
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
from volatility3.framework import constants, deprecation, interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
@@ -18,27 +18,41 @@ class Lsmod(plugins.PluginInterface):
|
||||
"""Lists loaded kernel modules."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (3, 0, 1)
|
||||
_version = (3, 0, 3)
|
||||
|
||||
run = linux_utilities_modules.ModuleDisplayPlugin.run
|
||||
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
|
||||
implementation = linux_utilities_modules.Modules.list_modules
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=constants.architectures.LINUX_ARCHS,
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules",
|
||||
component=linux_utilities_modules.Modules,
|
||||
version=(3, 0, 0),
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules_module_display_plugin",
|
||||
component=linux_utilities_modules.ModuleDisplayPlugin,
|
||||
version=(1, 0, 0),
|
||||
version=(2, 0, 0),
|
||||
),
|
||||
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Extract listed modules",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.list_modules,
|
||||
replacement_version=(3, 0, 0),
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
)
|
||||
def list_modules(
|
||||
cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str
|
||||
@@ -46,3 +60,18 @@ class Lsmod(plugins.PluginInterface):
|
||||
return linux_utilities_modules.Modules.list_modules(
|
||||
context, vmlinux_module_name
|
||||
)
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
linux_utilities_modules.ModuleDisplayPlugin.columns_results,
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
def _generator(self):
|
||||
yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results(
|
||||
self.context,
|
||||
self.implementation,
|
||||
self.config["kernel"],
|
||||
self.config["dump"],
|
||||
self.open,
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists open files for each processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (2, 0, 2)
|
||||
_version = (2, 1, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -137,6 +137,12 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
element_type=int,
|
||||
optional=True,
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="files_only",
|
||||
description="Include only file descriptors of type file",
|
||||
optional=True,
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@@ -145,6 +151,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
context: interfaces.context.ContextInterface,
|
||||
vmlinux_module_name: str,
|
||||
filter_func: Callable[[int], bool] = lambda _: False,
|
||||
include_files_only: bool = False,
|
||||
) -> Iterable[FDInternal]:
|
||||
"""Enumerates open file descriptors in tasks
|
||||
|
||||
@@ -167,16 +174,20 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0]
|
||||
|
||||
fd_generator = linux.LinuxUtilities.files_descriptors_for_process(
|
||||
context, linuxutils_symbol_table, task
|
||||
context, linuxutils_symbol_table, task, files_only=include_files_only
|
||||
)
|
||||
|
||||
for fd_fields in fd_generator:
|
||||
yield FDInternal(task=task, fd_fields=fd_fields)
|
||||
|
||||
def _generator(self, pids, vmlinux_module_name):
|
||||
def _generator(self, pids, vmlinux_module_name, include_files_only):
|
||||
filter_func = pslist.PsList.create_pid_filter(pids)
|
||||
|
||||
for fd_internal in self.list_fds(
|
||||
self.context, vmlinux_module_name, filter_func=filter_func
|
||||
self.context,
|
||||
vmlinux_module_name,
|
||||
filter_func=filter_func,
|
||||
include_files_only=include_files_only,
|
||||
):
|
||||
fd_user = fd_internal.to_user()
|
||||
yield (0, dataclasses.astuple(fd_user))
|
||||
@@ -184,6 +195,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
def run(self):
|
||||
pids = self.config.get("pid", None)
|
||||
vmlinux_module_name = self.config["kernel"]
|
||||
include_files_only = self.config.get("files_only")
|
||||
|
||||
tree_grid_args = [
|
||||
("PID", int),
|
||||
@@ -201,7 +213,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
("Size", int),
|
||||
]
|
||||
return renderers.TreeGrid(
|
||||
tree_grid_args, self._generator(pids, vmlinux_module_name)
|
||||
tree_grid_args,
|
||||
self._generator(
|
||||
pids, vmlinux_module_name, include_files_only=include_files_only
|
||||
),
|
||||
)
|
||||
|
||||
def generate_timeline(self):
|
||||
@@ -220,5 +235,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
|
||||
yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time
|
||||
yield (
|
||||
description,
|
||||
timeliner.TimeLinerType.MODIFIED,
|
||||
fd_user.modification_time,
|
||||
)
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
"""A module containing a plugin that verifies the operation function
|
||||
pointers of network protocols."""
|
||||
|
||||
import logging
|
||||
from typing import List, Tuple, Generator
|
||||
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Generator
|
||||
from typing import Dict, Generator, List
|
||||
|
||||
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
from volatility3.framework import constants, deprecation, interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Check_modules(plugins.PluginInterface):
|
||||
class Check_modules(interfaces.plugins.PluginInterface):
|
||||
"""Compares module list to sysfs info, if available"""
|
||||
|
||||
_version = (3, 0, 1)
|
||||
@@ -23,7 +22,7 @@ class Check_modules(plugins.PluginInterface):
|
||||
|
||||
@classmethod
|
||||
def compare_kset_and_lsmod(
|
||||
cls, context: str, vmlinux_name: str
|
||||
cls, context: interfaces.context.ContextInterface, vmlinux_name: str
|
||||
) -> Generator[extensions.module, None, None]:
|
||||
kset_modules = linux_utilities_modules.Modules.get_kset_modules(
|
||||
context=context, vmlinux_name=vmlinux_name
|
||||
@@ -39,13 +38,16 @@ class Check_modules(plugins.PluginInterface):
|
||||
for mod_name in set(kset_modules.keys()).difference(lsmod_modules):
|
||||
yield kset_modules[mod_name]
|
||||
|
||||
run = linux_utilities_modules.ModuleDisplayPlugin.run
|
||||
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
|
||||
implementation = compare_kset_and_lsmod
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=constants.architectures.LINUX_ARCHS,
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="modules",
|
||||
component=linux_utilities_modules.Modules,
|
||||
@@ -54,17 +56,38 @@ class Check_modules(plugins.PluginInterface):
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules_module_display_plugin",
|
||||
component=linux_utilities_modules.ModuleDisplayPlugin,
|
||||
version=(1, 0, 0),
|
||||
version=(2, 0, 0),
|
||||
),
|
||||
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Extract listed modules",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_kset_modules,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
def get_kset_modules(
|
||||
cls, context: interfaces.context.ContextInterface, vmlinux_name: str
|
||||
) -> Dict[str, extensions.module]:
|
||||
return linux_utilities_modules.Modules.get_kset_modules(context, vmlinux_name)
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
linux_utilities_modules.ModuleDisplayPlugin.columns_results,
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
def _generator(self):
|
||||
yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results(
|
||||
self.context,
|
||||
self.implementation,
|
||||
self.config["kernel"],
|
||||
self.config["dump"],
|
||||
self.open,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""A module containing a plugin that checks the system call table for hooks."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from typing import List, Set, Tuple, Iterable
|
||||
from typing import Generator, Iterable, List, Set, Tuple
|
||||
|
||||
from volatility3.framework import (
|
||||
constants,
|
||||
deprecation,
|
||||
exceptions,
|
||||
interfaces,
|
||||
renderers,
|
||||
)
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.symbols.linux.utilities import (
|
||||
modules as linux_utilities_modules,
|
||||
)
|
||||
from volatility3.framework import interfaces, exceptions, deprecation
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,12 +25,12 @@ class Hidden_modules(plugins.PluginInterface):
|
||||
"""Carves memory to find hidden kernel modules"""
|
||||
|
||||
_required_framework_version = (2, 25, 0)
|
||||
_version = (3, 0, 2)
|
||||
_version = (3, 0, 3)
|
||||
|
||||
@classmethod
|
||||
def find_hidden_modules(
|
||||
cls, context, vmlinux_module_name: str
|
||||
) -> extensions.module:
|
||||
) -> Generator[extensions.module, None, None]:
|
||||
if context.symbol_space.verify_table_versions(
|
||||
"dwarf2json", lambda version, _: (not version) or version < (0, 8, 0)
|
||||
):
|
||||
@@ -81,29 +88,38 @@ class Hidden_modules(plugins.PluginInterface):
|
||||
vmlinux_module_name, known_module_addresses, modules_memory_boundaries
|
||||
)
|
||||
|
||||
run = linux_utilities_modules.ModuleDisplayPlugin.run
|
||||
_generator = linux_utilities_modules.ModuleDisplayPlugin.generator
|
||||
implementation = find_hidden_modules
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Linux kernel",
|
||||
architectures=constants.architectures.LINUX_ARCHS,
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules_module_display_plugin",
|
||||
component=linux_utilities_modules.ModuleDisplayPlugin,
|
||||
version=(1, 0, 0),
|
||||
version=(2, 0, 0),
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linux_utilities_modules",
|
||||
component=linux_utilities_modules.Modules,
|
||||
version=(3, 0, 1),
|
||||
),
|
||||
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Extract listed modules",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
def get_modules_memory_boundaries(
|
||||
@@ -116,7 +132,7 @@ class Hidden_modules(plugins.PluginInterface):
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_module_address_alignment,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@classmethod
|
||||
@@ -144,13 +160,13 @@ class Hidden_modules(plugins.PluginInterface):
|
||||
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.get_hidden_modules,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
@staticmethod
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.validate_alignment_patterns,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
replacement_version=(3, 0, 0),
|
||||
)
|
||||
def _validate_alignment_patterns(
|
||||
@@ -195,3 +211,18 @@ class Hidden_modules(plugins.PluginInterface):
|
||||
)
|
||||
}
|
||||
return known_module_addresses
|
||||
|
||||
def run(self):
|
||||
return renderers.TreeGrid(
|
||||
linux_utilities_modules.ModuleDisplayPlugin.columns_results,
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
def _generator(self):
|
||||
yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results(
|
||||
self.context,
|
||||
self.implementation,
|
||||
self.config["kernel"],
|
||||
self.config["dump"],
|
||||
self.open,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
"""Lists process memory ranges that potentially contain injected code."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 3)
|
||||
_version = (1, 0, 4)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -37,6 +37,18 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
element_type=int,
|
||||
optional=True,
|
||||
),
|
||||
requirements.IntRequirement(
|
||||
name="dump-size",
|
||||
description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes",
|
||||
optional=True,
|
||||
default=64,
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="dump-page",
|
||||
description="Dump each dirty page and content - Default off",
|
||||
optional=True,
|
||||
default=False,
|
||||
),
|
||||
]
|
||||
|
||||
def _list_injections(
|
||||
@@ -51,14 +63,36 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
dump_size = self.config["dump-size"]
|
||||
|
||||
# Dumping page defaults to off, as in case a whole r-xp region is dirty
|
||||
# this would likely dump 1000's of pages which might not always be wise nor necessary
|
||||
|
||||
dump_page = self.config["dump-page"]
|
||||
|
||||
for vma in task.mm.get_vma_iter():
|
||||
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 is_suspicious returns true, this means at least one page
|
||||
# in the region is dirty. If dump_page is true, then we dump
|
||||
# all dirty pages
|
||||
|
||||
if vma.is_suspicious(proc_layer) and vma_name != "[vdso]":
|
||||
data = proc_layer.read(vma.vm_start, 64, pad=True)
|
||||
yield vma, vma_name, data
|
||||
malicious_pages = vma.get_malicious_pages(proc_layer)
|
||||
offset = 0
|
||||
if dump_page:
|
||||
# Dumping each dirty page
|
||||
for page_addr in malicious_pages:
|
||||
offset = page_addr - vma.vm_start
|
||||
data = proc_layer.read(page_addr, dump_size, pad=True)
|
||||
yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset
|
||||
else:
|
||||
# Original behaviour - Dump the start of the region (not necessarily matching the dirty page)
|
||||
data = proc_layer.read(vma.vm_start, dump_size, pad=True)
|
||||
yield vma, vma_name, data, offset
|
||||
|
||||
def _generator(self, tasks):
|
||||
# determine if we're on a 32 or 64 bit kernel
|
||||
@@ -70,13 +104,15 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
for task in tasks:
|
||||
process_name = utility.array_to_string(task.comm)
|
||||
|
||||
for vma, vma_name, data in self._list_injections(task):
|
||||
for vma, vma_name, data, offset in self._list_injections(task):
|
||||
if is_32bit_arch:
|
||||
architecture = "intel"
|
||||
else:
|
||||
architecture = "intel64"
|
||||
|
||||
disasm = renderers.Disassembly(data, vma.vm_start, architecture)
|
||||
disasm = renderers.Disassembly(
|
||||
data, vma.vm_start + offset, architecture
|
||||
)
|
||||
|
||||
yield (
|
||||
0,
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from typing import List, Dict, Iterator
|
||||
from typing import Dict, Iterator, List
|
||||
|
||||
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
|
||||
|
||||
from volatility3.framework import interfaces, deprecation, renderers
|
||||
from volatility3.framework import deprecation, interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.constants import architectures
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.symbols.linux import extensions
|
||||
from volatility3.framework.constants import architectures
|
||||
from volatility3.framework.symbols.linux.utilities import tainting
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -66,7 +65,7 @@ spot modules presence and taints."""
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.flatten_run_modules_results,
|
||||
replacement_version=(3, 0, 0),
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
)
|
||||
def flatten_run_modules_results(
|
||||
cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True
|
||||
@@ -89,7 +88,7 @@ spot modules presence and taints."""
|
||||
@deprecation.deprecated_method(
|
||||
replacement=linux_utilities_modules.Modules.run_modules_scanners,
|
||||
replacement_version=(3, 0, 0),
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
)
|
||||
def run_modules_scanners(
|
||||
cls,
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
from dataclasses import dataclass, field
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterator, List, Optional, Tuple
|
||||
|
||||
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
|
||||
from typing import Iterator, List, Tuple, Optional
|
||||
from volatility3 import framework
|
||||
from volatility3.framework import (
|
||||
constants,
|
||||
deprecation,
|
||||
exceptions,
|
||||
interfaces,
|
||||
renderers,
|
||||
exceptions,
|
||||
deprecation,
|
||||
)
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.symbols.linux import network
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -223,7 +223,16 @@ class AbstractNetfilter(ABC):
|
||||
)
|
||||
hooked = module_info is None
|
||||
|
||||
yield netns, proto_name, hook_name, priority, hook_ops_hook, module_info, symbol_name, hooked
|
||||
yield (
|
||||
netns,
|
||||
proto_name,
|
||||
hook_name,
|
||||
priority,
|
||||
hook_ops_hook,
|
||||
module_info,
|
||||
symbol_name,
|
||||
hooked,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
@@ -304,7 +313,7 @@ class AbstractNetfilter(ABC):
|
||||
return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET")
|
||||
|
||||
@deprecation.method_being_removed(
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-03-25",
|
||||
message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`",
|
||||
)
|
||||
def get_module_name_for_address(self, addr) -> str:
|
||||
|
||||
@@ -100,11 +100,14 @@ class Tty_Check(plugins.PluginInterface):
|
||||
else:
|
||||
module_name = renderers.NotAvailableValue()
|
||||
|
||||
yield 0, (
|
||||
name,
|
||||
format_hints.Hex(recv_buf),
|
||||
module_name,
|
||||
symbol_name or renderers.NotAvailableValue(),
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
name,
|
||||
format_hints.Hex(recv_buf),
|
||||
module_name,
|
||||
symbol_name or renderers.NotAvailableValue(),
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -75,10 +75,13 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
|
||||
with self.open(file_name) as file_handle:
|
||||
file_handle.write(elf_data)
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(base_address),
|
||||
len(elf_data),
|
||||
file_handle.preferred_filename,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(base_address),
|
||||
len(elf_data),
|
||||
file_handle.preferred_filename,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -386,7 +386,11 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
inode_out = inode_in.to_user(vmlinux_layer)
|
||||
description = f"Cached Inode for {inode_out.path}"
|
||||
yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time
|
||||
yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time
|
||||
yield (
|
||||
description,
|
||||
timeliner.TimeLinerType.MODIFIED,
|
||||
inode_out.modification_time,
|
||||
)
|
||||
yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time
|
||||
|
||||
@classmethod
|
||||
@@ -813,7 +817,6 @@ class RecoverFs(plugins.PluginInterface):
|
||||
|
||||
visited_paths = seen_prefixes = set()
|
||||
for inode_in in inodes_iter:
|
||||
|
||||
# Code is slightly duplicated here with the if-block below.
|
||||
# However this prevents unneeded tar manipulation if fifo
|
||||
# or sock inodes come through for example.
|
||||
|
||||
@@ -225,18 +225,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
task_euid = self._format_cred(task_fields.euid)
|
||||
task_egid = self._format_cred(task_fields.egid)
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(task_fields.offset),
|
||||
task_fields.user_pid,
|
||||
task_fields.user_tid,
|
||||
task_fields.user_ppid,
|
||||
task_fields.name,
|
||||
task_uid,
|
||||
task_gid,
|
||||
task_euid,
|
||||
task_egid,
|
||||
task_fields.creation_time or renderers.NotAvailableValue(),
|
||||
file_output,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(task_fields.offset),
|
||||
task_fields.user_pid,
|
||||
task_fields.user_tid,
|
||||
task_fields.user_ppid,
|
||||
task_fields.name,
|
||||
task_uid,
|
||||
task_gid,
|
||||
task_euid,
|
||||
task_egid,
|
||||
task_fields.creation_time or renderers.NotAvailableValue(),
|
||||
file_output,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -70,7 +70,6 @@ class PerfEvents(plugins.PluginInterface):
|
||||
for task in pslist.PsList.list_tasks(
|
||||
context, vmlinux_module_name, include_threads=True
|
||||
):
|
||||
|
||||
# walk the list of perf_event entries for this process
|
||||
for event in task.perf_event_list.to_list(
|
||||
vmlinux.symbol_table_name + constants.BANG + "perf_event", "owner_entry"
|
||||
|
||||
@@ -64,7 +64,6 @@ class VmaRegExScan(plugins.PluginInterface):
|
||||
vollog.debug(f"RegEx Pattern: {regex_pattern}")
|
||||
|
||||
for task in tasks:
|
||||
|
||||
if not task.mm:
|
||||
continue
|
||||
name = utility.array_to_string(task.comm)
|
||||
@@ -106,12 +105,15 @@ class VmaRegExScan(plugins.PluginInterface):
|
||||
bytes_result = result_data
|
||||
|
||||
user_pid = task.tgid
|
||||
yield 0, (
|
||||
user_pid,
|
||||
name,
|
||||
format_hints.Hex(offset),
|
||||
text_result,
|
||||
bytes_result,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
user_pid,
|
||||
name,
|
||||
format_hints.Hex(offset),
|
||||
text_result,
|
||||
bytes_result,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -103,12 +103,15 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
|
||||
layer_name=proc_layer.name,
|
||||
length=len(value),
|
||||
)
|
||||
yield 0, (
|
||||
format_hints.Hex(offset),
|
||||
task.tgid,
|
||||
rule_name,
|
||||
name,
|
||||
layer_data,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(offset),
|
||||
task.tgid,
|
||||
rule_name,
|
||||
name,
|
||||
layer_data,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
"""A module containing a collection of plugins that produce data typically
|
||||
found in Mac's lsmod command."""
|
||||
|
||||
from typing import Set
|
||||
|
||||
from volatility3.framework import renderers, interfaces, exceptions
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
"""A module containing a collection of plugins that produce data typically
|
||||
found in Mac's mount command."""
|
||||
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
"""In-memory artifacts from OSX systems."""
|
||||
|
||||
from typing import Iterator, Tuple, Any, Generator, List
|
||||
|
||||
from volatility3.framework import exceptions, renderers, interfaces
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TextIO,
|
||||
)
|
||||
from volatility3.framework import interfaces, renderers
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.cli import text_renderer
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
ARROW_PRESENT = False
|
||||
try:
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
ARROW_PRESENT = True
|
||||
except ImportError:
|
||||
vollog.debug("Arrow/Parquet libraries not found")
|
||||
|
||||
|
||||
class ArrowRenderer(text_renderer.CLIRenderer):
|
||||
"""Renderer that outputs Arrow IPC format data."""
|
||||
|
||||
name = "arrow"
|
||||
structured_output = True
|
||||
_version = (1, 0, 0)
|
||||
|
||||
def __init__(
|
||||
self, options: Optional[List[interfaces.renderers.RenderOption]] = None
|
||||
) -> None:
|
||||
super().__init__(options)
|
||||
|
||||
if not ARROW_PRESENT:
|
||||
raise RuntimeError("Arrow output format requires the pyarrow package")
|
||||
|
||||
self._to_arrow_type = {
|
||||
renderers.Disassembly: pa.utf8,
|
||||
bool: pa.bool_,
|
||||
int: pa.int64,
|
||||
float: pa.float64,
|
||||
str: pa.utf8,
|
||||
datetime.datetime: lambda: pa.timestamp("ms"),
|
||||
format_hints.Bin: pa.uint64,
|
||||
format_hints.Hex: pa.uint64,
|
||||
format_hints.MultiTypeData: pa.utf8,
|
||||
format_hints.HexBytes: pa.binary,
|
||||
renderers.LayerData: pa.binary,
|
||||
bytes: pa.binary,
|
||||
}
|
||||
|
||||
# indicates if the output from the plugin is nested, e.g., pstree
|
||||
# which would then need to be flattened
|
||||
self._is_tree_result = False
|
||||
self._node_id_counter = 0
|
||||
|
||||
def get_render_options(self) -> List[interfaces.renderers.RenderOption]:
|
||||
return []
|
||||
|
||||
def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema":
|
||||
fields = []
|
||||
for column in grid.columns:
|
||||
arrow_type = self._to_arrow_type[column.type]
|
||||
fields.append(pa.field(column.name, arrow_type()))
|
||||
|
||||
# if the output is nested, e.g., windows.pstree
|
||||
if self._is_tree_result:
|
||||
fields.append(pa.field("_vol_id", pa.uint64()))
|
||||
fields.append(pa.field("_vol_parent_id", pa.uint64()))
|
||||
|
||||
return pa.schema(fields)
|
||||
|
||||
def _flatten_tree_structure(self, nested: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Flattens a list of nested dicts using the `__children` key.
|
||||
|
||||
Each node gets a `_vol_id` and a `_vol_parent_id` to preserve
|
||||
the original tree structure in a flat format suitable for tabular output.
|
||||
|
||||
Args:
|
||||
nested: A list of dicts with optional `__children` lists (tree nodes).
|
||||
|
||||
Returns:
|
||||
A flat list of dicts with `_vol_id` and `_vol_parent_id`.
|
||||
"""
|
||||
rows = []
|
||||
self._node_id_counter = 0
|
||||
|
||||
def _process_node(node: Dict, parent_id: Optional[int]):
|
||||
current_id = self._node_id_counter
|
||||
self._node_id_counter += 1
|
||||
|
||||
entry = {k: v for k, v in node.items() if k != "__children"}
|
||||
entry["_vol_id"] = current_id
|
||||
entry["_vol_parent_id"] = parent_id
|
||||
rows.append(entry)
|
||||
|
||||
for child in node.get("__children", []):
|
||||
_process_node(child, current_id)
|
||||
|
||||
for root in nested:
|
||||
_process_node(root, None)
|
||||
|
||||
return rows
|
||||
|
||||
def output_result(self, schema: "pa.Schema", outfd: TextIO, result):
|
||||
"""Outputs the JSON data to a file in a particular format"""
|
||||
|
||||
if self._is_tree_result:
|
||||
result = self._flatten_tree_structure(result)
|
||||
|
||||
t = pa.Table.from_pylist(result, schema=schema)
|
||||
self.write_table(t, outfd)
|
||||
|
||||
def write_table(self, t: "pa.Table", outfd: TextIO) -> None:
|
||||
buf = pa.BufferOutputStream()
|
||||
|
||||
writer = pa.ipc.new_stream(buf, t.schema)
|
||||
writer.write_table(t)
|
||||
writer.close()
|
||||
|
||||
# Get the buffer bytes and write to output
|
||||
buf_bytes = buf.getvalue().to_pybytes()
|
||||
outfd.buffer.write(buf_bytes)
|
||||
|
||||
def render(self, grid: interfaces.renderers.TreeGrid):
|
||||
outfd = sys.stdout
|
||||
final_output: Tuple[
|
||||
Dict[str, List[interfaces.renderers.TreeNode]],
|
||||
List[interfaces.renderers.TreeNode],
|
||||
] = ({}, [])
|
||||
|
||||
ignore_columns = self.ignored_columns(grid)
|
||||
|
||||
def visitor(
|
||||
node: interfaces.renderers.TreeNode,
|
||||
accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]],
|
||||
) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]:
|
||||
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
|
||||
acc_map, final_tree = accumulator
|
||||
node_dict: Dict[str, Any] = {"__children": []}
|
||||
line = []
|
||||
for column_index, column in enumerate(grid.columns):
|
||||
if column in ignore_columns:
|
||||
continue
|
||||
|
||||
data = list(node.values)[column_index]
|
||||
|
||||
if isinstance(data, interfaces.renderers.BaseAbsentValue):
|
||||
data = None
|
||||
|
||||
if isinstance(data, renderers.Disassembly):
|
||||
data = text_renderer.display_disassembly(data)
|
||||
|
||||
if isinstance(data, renderers.LayerData):
|
||||
data = text_renderer.LayerDataRenderer().render_bytes(data)[0]
|
||||
|
||||
node_dict[column.name] = data
|
||||
line.append(data)
|
||||
|
||||
if self.filter and self.filter.filter(line):
|
||||
return accumulator
|
||||
|
||||
if node.parent:
|
||||
acc_map[node.parent.path]["__children"].append(node_dict)
|
||||
self._is_tree_result = True
|
||||
else:
|
||||
final_tree.append(node_dict)
|
||||
acc_map[node.path] = node_dict
|
||||
|
||||
return (acc_map, final_tree)
|
||||
|
||||
if not grid.populated:
|
||||
grid.populate(visitor, final_output)
|
||||
else:
|
||||
grid.visit(node=None, function=visitor, initial_accumulator=final_output)
|
||||
|
||||
schema = self.to_arrow_schema(grid)
|
||||
self.output_result(schema, outfd, final_output[1])
|
||||
|
||||
|
||||
class ParquetRenderer(ArrowRenderer):
|
||||
"""Renderer that outputs Parquet format data."""
|
||||
|
||||
name = "parquet"
|
||||
structured_output = True
|
||||
_version = (1, 0, 0)
|
||||
|
||||
def get_render_options(self) -> List[interfaces.renderers.RenderOption]:
|
||||
return []
|
||||
|
||||
def write_table(self, table: "pa.Table", outfd: TextIO) -> None:
|
||||
"""
|
||||
Writes a table to stdout using the Parquet format.
|
||||
|
||||
Args:
|
||||
t: The Arrow table to write
|
||||
outfd: The output file descriptor
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
"""
|
||||
# Write DataFrame to a temporary file-like object
|
||||
buf = pa.BufferOutputStream()
|
||||
pq.write_table(table, buf, compression="snappy")
|
||||
|
||||
# Get the buffer as a bytes object
|
||||
buf_bytes = buf.getvalue().to_pybytes()
|
||||
outfd.buffer.write(buf_bytes)
|
||||
@@ -2,7 +2,8 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
|
||||
from volatility3.framework import deprecation, interfaces
|
||||
from volatility3.plugins.windows.registry import amcache
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -12,7 +13,7 @@ class Amcache(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=amcache.Amcache,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-09-25",
|
||||
):
|
||||
"""Extract information on executed applications from the AmCache (deprecated)."""
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
|
||||
from volatility3.framework import deprecation, interfaces
|
||||
from volatility3.plugins.windows.registry import cachedump
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -12,7 +13,7 @@ class Cachedump(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=cachedump.Cachedump,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-09-25",
|
||||
):
|
||||
"""Dumps lsa secrets from memory (deprecated)"""
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
def _create_default_scan_constraints(
|
||||
context: interfaces.context.ContextInterface, symbol_table: str
|
||||
) -> List[poolscanner.PoolConstraint]:
|
||||
|
||||
shutdown_packet_size = context.symbol_space.get_type(
|
||||
symbol_table + constants.BANG + "_SHUTDOWN_PACKET"
|
||||
).size
|
||||
@@ -590,7 +589,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
|
||||
except exceptions.InvalidAddressException:
|
||||
component = renderers.UnreadableValue()
|
||||
|
||||
yield "KeBugCheckReasonCallbackListHead", callback.CallbackRoutine, component
|
||||
yield (
|
||||
"KeBugCheckReasonCallbackListHead",
|
||||
callback.CallbackRoutine,
|
||||
component,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list_bugcheck_callbacks(
|
||||
|
||||
@@ -77,6 +77,11 @@ class DeskScan(desktops.Desktops):
|
||||
continue
|
||||
|
||||
for _thread, process_name, process_pid in desktop.get_threads():
|
||||
yield format_hints.Hex(
|
||||
desktop.vol.offset
|
||||
), winsta_name, session_id, desktop_name, process_name, process_pid
|
||||
yield (
|
||||
format_hints.Hex(desktop.vol.offset),
|
||||
winsta_name,
|
||||
session_id,
|
||||
desktop_name,
|
||||
process_name,
|
||||
process_pid,
|
||||
)
|
||||
|
||||
@@ -63,9 +63,14 @@ class Desktops(interfaces.plugins.PluginInterface):
|
||||
for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name):
|
||||
# for each desktop, walk its threads
|
||||
for _thread, process_name, process_pid in desktop.get_threads():
|
||||
yield format_hints.Hex(
|
||||
desktop.vol.offset
|
||||
), station_name, session_id, desktop_name, process_name, process_pid
|
||||
yield (
|
||||
format_hints.Hex(desktop.vol.offset),
|
||||
station_name,
|
||||
session_id,
|
||||
desktop_name,
|
||||
process_name,
|
||||
process_pid,
|
||||
)
|
||||
|
||||
def _generator(self):
|
||||
kernel_name = self.config["kernel"]
|
||||
|
||||
@@ -22,7 +22,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists the loaded DLLs in a particular windows memory image."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (3, 0, 0)
|
||||
_version = (3, 0, 1)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -173,6 +173,10 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
except exceptions.InvalidAddressException:
|
||||
size_of_image = renderers.NotAvailableValue()
|
||||
|
||||
LoadCount = entry.get_load_count()
|
||||
if LoadCount is None:
|
||||
LoadCount = renderers.NotAvailableValue()
|
||||
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
@@ -186,6 +190,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
size_of_image,
|
||||
BaseDllName,
|
||||
FullDllName,
|
||||
LoadCount,
|
||||
DllLoadTime,
|
||||
file_output,
|
||||
),
|
||||
@@ -232,6 +237,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
("Size", format_hints.Hex),
|
||||
("Name", str),
|
||||
("Path", str),
|
||||
("LoadCount", int),
|
||||
("LoadTime", datetime.datetime),
|
||||
("File output", str),
|
||||
],
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
|
||||
from volatility3.framework import deprecation, interfaces
|
||||
from volatility3.plugins.windows.registry import hashdump
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -12,7 +13,7 @@ class Hashdump(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=hashdump.Hashdump,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-09-25",
|
||||
):
|
||||
"""Dumps user hashes from memory (deprecated)"""
|
||||
|
||||
|
||||
@@ -96,7 +96,6 @@ class KPCRs(interfaces.plugins.PluginInterface):
|
||||
yield kpcr, kpcr.member(kpcr_member)
|
||||
|
||||
def _generator(self) -> Iterator[Tuple]:
|
||||
|
||||
for kpcr, current_prcb in self.list_kpcrs(self.context, self.config["kernel"]):
|
||||
yield (
|
||||
0,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
|
||||
from volatility3.framework import deprecation, interfaces
|
||||
from volatility3.plugins.windows.registry import lsadump
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -12,7 +13,7 @@ class Lsadump(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=lsadump.Lsadump,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-09-25",
|
||||
):
|
||||
"""Dumps lsa secrets from memory (deprecated)"""
|
||||
|
||||
|
||||
@@ -451,12 +451,15 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
|
||||
|
||||
address, disasm_bytes = syscall_info
|
||||
|
||||
yield 0, (
|
||||
proc_name,
|
||||
proc.UniqueProcessId,
|
||||
vad_path,
|
||||
format_hints.Hex(address),
|
||||
disasm_bytes,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
proc_name,
|
||||
proc.UniqueProcessId,
|
||||
vad_path,
|
||||
format_hints.Hex(address),
|
||||
disasm_bytes,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self) -> renderers.TreeGrid:
|
||||
|
||||
@@ -198,10 +198,13 @@ class HollowProcesses(interfaces.plugins.PluginInterface):
|
||||
|
||||
for check in checks:
|
||||
for note in check(proc, vads, dlls):
|
||||
yield 0, (
|
||||
pid,
|
||||
proc_name,
|
||||
note,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
pid,
|
||||
proc_name,
|
||||
note,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -92,8 +92,11 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
for vad, data_object in cls.list_injection_sites(
|
||||
context, kernel_layer_name, symbol_table, proc
|
||||
):
|
||||
yield vad, data_object.context.layers[data_object.layer_name].read(
|
||||
data_object.offset, data_object.length
|
||||
yield (
|
||||
vad,
|
||||
data_object.context.layers[data_object.layer_name].read(
|
||||
data_object.offset, data_object.length
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import logging
|
||||
from typing import List, Union, Tuple
|
||||
|
||||
from volatility3.framework import interfaces, renderers, exceptions
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.objects import utility
|
||||
from volatility3.plugins.windows import pslist
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# https://www.ired.team/offensive-security/defense-evasion/masquerading-processes-in-userland-through-_peb
|
||||
# https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Masquerade-PEB.ps1
|
||||
class PebMasquerade(interfaces.plugins.PluginInterface):
|
||||
"""Detects potential process name spoofing by comparing EPROCESS and PEB data."""
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_required_framework_version = (2, 27, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Windows kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="pslist", component=pslist.PsList, version=(3, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
element_type=int,
|
||||
description="Process ID to include (all other processes are excluded)",
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[
|
||||
Union[str, renderers.NotAvailableValue],
|
||||
Union[str, renderers.NotAvailableValue],
|
||||
Union[str, renderers.NotAvailableValue],
|
||||
Union[str, renderers.NotAvailableValue],
|
||||
]:
|
||||
"""Extract process names and related information from various sources (EPROCESS and PEB).
|
||||
|
||||
Args:
|
||||
proc: The process object
|
||||
|
||||
Returns:
|
||||
tuple: (eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline)
|
||||
"""
|
||||
eprocess_imagefilename = renderers.NotAvailableValue()
|
||||
eprocess_seaudit_imagefilename = renderers.NotAvailableValue()
|
||||
peb_imagefilepath = renderers.NotAvailableValue()
|
||||
peb_cmdline = renderers.NotAvailableValue()
|
||||
|
||||
try:
|
||||
eprocess_imagefilename = utility.array_to_string(proc.ImageFileName)
|
||||
except (AttributeError, exceptions.InvalidAddressException):
|
||||
vollog.debug(
|
||||
"Unable to read EPROCESS.ImageFileName for PID %d", proc.UniqueProcessId
|
||||
)
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"Error reading EPROCESS.ImageFileName for PID %d: %s",
|
||||
proc.UniqueProcessId,
|
||||
str(e),
|
||||
)
|
||||
|
||||
try:
|
||||
audit = proc.SeAuditProcessCreationInfo.ImageFileName.Name
|
||||
audit_string = audit.get_string()
|
||||
if audit_string:
|
||||
eprocess_seaudit_imagefilename = audit_string
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.debug(
|
||||
"Unable to read SeAuditProcessCreationInfo.ImageFileName for PID %d",
|
||||
proc.UniqueProcessId,
|
||||
)
|
||||
except AttributeError:
|
||||
vollog.debug(
|
||||
"SeAuditProcessCreationInfo structure not available for PID %d",
|
||||
proc.UniqueProcessId,
|
||||
)
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"Error reading SeAuditProcessCreationInfo for PID %d: %s",
|
||||
proc.UniqueProcessId,
|
||||
str(e),
|
||||
)
|
||||
|
||||
try:
|
||||
peb = proc.get_peb()
|
||||
if peb and peb.ProcessParameters:
|
||||
# Get ImagePathName
|
||||
try:
|
||||
image_path_str = peb.ProcessParameters.ImagePathName.get_string()
|
||||
if image_path_str:
|
||||
peb_imagefilepath = image_path_str
|
||||
except (AttributeError, exceptions.InvalidAddressException):
|
||||
vollog.debug(
|
||||
"Unable to read PEB.ImagePathName for PID %d",
|
||||
proc.UniqueProcessId,
|
||||
)
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"Error reading PEB.ImagePathName for PID %d: %s",
|
||||
proc.UniqueProcessId,
|
||||
str(e),
|
||||
)
|
||||
|
||||
try:
|
||||
cmdline_str = peb.ProcessParameters.CommandLine.get_string()
|
||||
if cmdline_str:
|
||||
peb_cmdline = cmdline_str
|
||||
except (AttributeError, exceptions.InvalidAddressException):
|
||||
vollog.debug(
|
||||
"Unable to read PEB.ProcessParameters.CommandLine for PID %d",
|
||||
proc.UniqueProcessId,
|
||||
)
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"Error reading PEB.ProcessParameters.CommandLine for PID %d: %s",
|
||||
proc.UniqueProcessId,
|
||||
str(e),
|
||||
)
|
||||
except (AttributeError, exceptions.InvalidAddressException):
|
||||
# Important for cases where PEB does not exist or is inaccessible (e.g SYSTEM process)
|
||||
vollog.debug("Unable to access PEB for PID %d", proc.UniqueProcessId)
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e)
|
||||
)
|
||||
|
||||
return (
|
||||
eprocess_imagefilename,
|
||||
eprocess_seaudit_imagefilename,
|
||||
peb_imagefilepath,
|
||||
peb_cmdline,
|
||||
)
|
||||
|
||||
def _generator(self, pids, context, kernel_module_name):
|
||||
pid_filter = pslist.PsList.create_pid_filter(pids)
|
||||
|
||||
for proc in pslist.PsList.list_processes(
|
||||
context=context,
|
||||
kernel_module_name=kernel_module_name,
|
||||
filter_func=pid_filter,
|
||||
):
|
||||
proc_id = proc.UniqueProcessId
|
||||
try:
|
||||
peb = proc.get_peb()
|
||||
except (exceptions.InvalidAddressException, AttributeError):
|
||||
vollog.debug(
|
||||
"Unable to access PEB for PID %d, skipping process", proc_id
|
||||
)
|
||||
peb_imagefilepath_length_check = False
|
||||
peb_cmdline_length_check = False
|
||||
(
|
||||
eprocess_imagefilename,
|
||||
eprocess_seaudit_imagefilename,
|
||||
peb_imagefilepath,
|
||||
peb_cmdline,
|
||||
) = PebMasquerade.get_process_names(proc)
|
||||
|
||||
if isinstance(peb_imagefilepath, str) and peb:
|
||||
try:
|
||||
# Length values are of type USHORT
|
||||
peb_imagefilepath_length = (
|
||||
peb.ProcessParameters.ImagePathName.Length // 2
|
||||
)
|
||||
peb_imagefilepath_maxlength = (
|
||||
peb.ProcessParameters.ImagePathName.MaximumLength // 2 - 1
|
||||
)
|
||||
|
||||
if (peb_imagefilepath_length != len(peb_imagefilepath)) or (
|
||||
peb_imagefilepath_maxlength != len(peb_imagefilepath)
|
||||
):
|
||||
peb_imagefilepath_length_check = True
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"PEB.ImagePathName Length comparison error for PID %d: %s",
|
||||
proc_id,
|
||||
str(e),
|
||||
)
|
||||
|
||||
if isinstance(peb_cmdline, str) and peb:
|
||||
try:
|
||||
# Length values are of type USHORT
|
||||
peb_cmdline_length = peb.ProcessParameters.CommandLine.Length // 2
|
||||
peb_cmdline_maxlength = (
|
||||
peb.ProcessParameters.CommandLine.MaximumLength // 2 - 1
|
||||
)
|
||||
|
||||
if (peb_cmdline_length != len(peb_cmdline)) or (
|
||||
peb_cmdline_maxlength != len(peb_cmdline)
|
||||
):
|
||||
peb_cmdline_length_check = True
|
||||
except Exception as e:
|
||||
vollog.warning(
|
||||
"PEB.CommandLine Length comparison error for PID %d: %s",
|
||||
proc_id,
|
||||
str(e),
|
||||
)
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
proc_id,
|
||||
eprocess_imagefilename,
|
||||
eprocess_seaudit_imagefilename,
|
||||
peb_imagefilepath,
|
||||
peb_cmdline_length_check,
|
||||
peb_imagefilepath_length_check,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
pids = self.config.get("pid", None)
|
||||
context = self.context
|
||||
kernel_module_name = self.config["kernel"]
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("PID", int),
|
||||
("EPROCESS_ImageFileName", str),
|
||||
("EPROCESS_SeAudit_ImageFileName", str),
|
||||
("PEB_ImageFilePath", str),
|
||||
("PEB_ImageFilePath_Spoofed", bool),
|
||||
("PEB_CommandLine_Spoofed", bool),
|
||||
],
|
||||
self._generator(pids, context, kernel_module_name),
|
||||
)
|
||||
@@ -149,9 +149,12 @@ class ProcessGhosting(interfaces.plugins.PluginInterface):
|
||||
for file_object_address, delete_pending, delete_on_close in cls._vad_checks(
|
||||
control_area, path
|
||||
):
|
||||
yield format_hints.Hex(
|
||||
file_object_address
|
||||
), delete_pending, delete_on_close, vad_base
|
||||
yield (
|
||||
format_hints.Hex(file_object_address),
|
||||
delete_pending,
|
||||
delete_on_close,
|
||||
vad_base,
|
||||
)
|
||||
|
||||
def _generator(self, procs):
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
@@ -187,14 +190,17 @@ class ProcessGhosting(interfaces.plugins.PluginInterface):
|
||||
else:
|
||||
path = renderers.NotAvailableValue()
|
||||
|
||||
yield 0, (
|
||||
pid,
|
||||
process_name,
|
||||
format_hints.Hex(base_address),
|
||||
format_hints.Hex(file_object_address),
|
||||
delete_pending or renderers.NotApplicableValue(),
|
||||
delete_on_close or renderers.NotApplicableValue(),
|
||||
path,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
pid,
|
||||
process_name,
|
||||
format_hints.Hex(base_address),
|
||||
format_hints.Hex(file_object_address),
|
||||
delete_pending or renderers.NotApplicableValue(),
|
||||
delete_on_close or renderers.NotApplicableValue(),
|
||||
path,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -648,12 +648,15 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
|
||||
csystem, cryptdll_base, cryptdll_size
|
||||
)
|
||||
|
||||
yield 0, (
|
||||
lsass_proc.UniqueProcessId,
|
||||
"lsass.exe",
|
||||
skeleton_key_present,
|
||||
format_hints.Hex(csystem.Initialize),
|
||||
format_hints.Hex(csystem.Decrypt),
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
lsass_proc.UniqueProcessId,
|
||||
"lsass.exe",
|
||||
skeleton_key_present,
|
||||
format_hints.Hex(csystem.Initialize),
|
||||
format_hints.Hex(csystem.Decrypt),
|
||||
),
|
||||
)
|
||||
|
||||
def _lsass_proc_filter(self, proc):
|
||||
|
||||
@@ -196,14 +196,17 @@ class SuspiciousThreads(interfaces.plugins.PluginInterface):
|
||||
for vad_path, note in self._check_thread_address(
|
||||
exe_path, ranges, address
|
||||
):
|
||||
yield 0, (
|
||||
proc_name,
|
||||
pid,
|
||||
tid,
|
||||
context,
|
||||
format_hints.Hex(address),
|
||||
vad_path,
|
||||
note,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
proc_name,
|
||||
pid,
|
||||
tid,
|
||||
context,
|
||||
format_hints.Hex(address),
|
||||
vad_path,
|
||||
note,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -132,19 +132,22 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
# There should only be one STANDARD_INFORMATION attribute, but we
|
||||
# do this just in case.
|
||||
for std_information in mft_record.standard_information_entries():
|
||||
yield 0, cls.MFTScanResult(
|
||||
format_hints.Hex(std_information.vol.offset),
|
||||
str(mft_record.get_signature()),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
renderers.NotApplicableValue(),
|
||||
"STANDARD_INFORMATION",
|
||||
conversion.wintime_to_datetime(std_information.CreationTime),
|
||||
conversion.wintime_to_datetime(std_information.ModifiedTime),
|
||||
conversion.wintime_to_datetime(std_information.UpdatedTime),
|
||||
conversion.wintime_to_datetime(std_information.AccessedTime),
|
||||
renderers.NotApplicableValue(),
|
||||
yield (
|
||||
0,
|
||||
cls.MFTScanResult(
|
||||
format_hints.Hex(std_information.vol.offset),
|
||||
str(mft_record.get_signature()),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
renderers.NotApplicableValue(),
|
||||
"STANDARD_INFORMATION",
|
||||
conversion.wintime_to_datetime(std_information.CreationTime),
|
||||
conversion.wintime_to_datetime(std_information.ModifiedTime),
|
||||
conversion.wintime_to_datetime(std_information.UpdatedTime),
|
||||
conversion.wintime_to_datetime(std_information.AccessedTime),
|
||||
renderers.NotApplicableValue(),
|
||||
),
|
||||
)
|
||||
except exceptions.InvalidAddressException:
|
||||
pass
|
||||
@@ -163,26 +166,28 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
# File Name Attribute
|
||||
try:
|
||||
for filename_info in mft_record.filename_entries():
|
||||
|
||||
# If we don't have a valid enum, coerce to hex so we can keep the record
|
||||
try:
|
||||
permissions = filename_info.Flags.lookup()
|
||||
except ValueError:
|
||||
permissions = hex(filename_info.Flags)
|
||||
|
||||
yield 1, cls.MFTScanResult(
|
||||
format_hints.Hex(filename_info.vol.offset),
|
||||
str(mft_record.get_signature()),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
permissions,
|
||||
"FILE_NAME",
|
||||
conversion.wintime_to_datetime(filename_info.CreationTime),
|
||||
conversion.wintime_to_datetime(filename_info.ModifiedTime),
|
||||
conversion.wintime_to_datetime(filename_info.UpdatedTime),
|
||||
conversion.wintime_to_datetime(filename_info.AccessedTime),
|
||||
filename_info.get_full_name(),
|
||||
yield (
|
||||
1,
|
||||
cls.MFTScanResult(
|
||||
format_hints.Hex(filename_info.vol.offset),
|
||||
str(mft_record.get_signature()),
|
||||
mft_record.RecordNumber,
|
||||
mft_record.LinkCount,
|
||||
mft_flag,
|
||||
permissions,
|
||||
"FILE_NAME",
|
||||
conversion.wintime_to_datetime(filename_info.CreationTime),
|
||||
conversion.wintime_to_datetime(filename_info.ModifiedTime),
|
||||
conversion.wintime_to_datetime(filename_info.UpdatedTime),
|
||||
conversion.wintime_to_datetime(filename_info.AccessedTime),
|
||||
filename_info.get_full_name(),
|
||||
),
|
||||
)
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
@@ -214,22 +219,25 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
# but in this case memory usage is so extreme due to the number of
|
||||
# records that it becomes necessary. The rich types are still
|
||||
# exposed through classmethods.
|
||||
yield level, (
|
||||
record.offset,
|
||||
record.record_type,
|
||||
int(record.record_number),
|
||||
int(record.link_count),
|
||||
record.mft_type,
|
||||
record.permissions,
|
||||
record.attribute_type,
|
||||
record.created,
|
||||
record.modified,
|
||||
record.updated,
|
||||
record.accessed,
|
||||
yield (
|
||||
level,
|
||||
(
|
||||
str(record.filename)
|
||||
if isinstance(record.filename, objects.String)
|
||||
else record.filename
|
||||
record.offset,
|
||||
record.record_type,
|
||||
int(record.record_number),
|
||||
int(record.link_count),
|
||||
record.mft_type,
|
||||
record.permissions,
|
||||
record.attribute_type,
|
||||
record.created,
|
||||
record.modified,
|
||||
record.updated,
|
||||
record.accessed,
|
||||
(
|
||||
str(record.filename)
|
||||
if isinstance(record.filename, objects.String)
|
||||
else record.filename
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -344,22 +352,25 @@ class ADS(interfaces.plugins.PluginInterface):
|
||||
# but in this case memory usage is so extreme due to the number of
|
||||
# records that it becomes necessary. The rich types are still
|
||||
# exposed through classmethods.
|
||||
yield 0, (
|
||||
record.offset,
|
||||
str(record.signature),
|
||||
int(record.record_number),
|
||||
record.attribute_type,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
str(record.filename)
|
||||
if isinstance(record.filename, objects.String)
|
||||
else record.filename
|
||||
record.offset,
|
||||
str(record.signature),
|
||||
int(record.record_number),
|
||||
record.attribute_type,
|
||||
(
|
||||
str(record.filename)
|
||||
if isinstance(record.filename, objects.String)
|
||||
else record.filename
|
||||
),
|
||||
(
|
||||
str(record.stream_name)
|
||||
if isinstance(record.stream_name, objects.String)
|
||||
else record.stream_name
|
||||
),
|
||||
record.content,
|
||||
),
|
||||
(
|
||||
str(record.stream_name)
|
||||
if isinstance(record.stream_name, objects.String)
|
||||
else record.stream_name
|
||||
),
|
||||
record.content,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
@@ -454,13 +465,16 @@ class ResidentData(interfaces.plugins.PluginInterface):
|
||||
# but in this case memory usage is so extreme due to the number of
|
||||
# records that it becomes necessary. The rich types are still
|
||||
# exposed through classmethods.
|
||||
yield 0, (
|
||||
resident_data_entry.offset,
|
||||
str(resident_data_entry.signature),
|
||||
int(resident_data_entry.record_number),
|
||||
resident_data_entry.attribute_type,
|
||||
str(resident_data_entry.filename),
|
||||
resident_data_entry.content,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
resident_data_entry.offset,
|
||||
str(resident_data_entry.signature),
|
||||
int(resident_data_entry.record_number),
|
||||
resident_data_entry.attribute_type,
|
||||
str(resident_data_entry.filename),
|
||||
resident_data_entry.content,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -119,13 +119,16 @@ class Modules(interfaces.plugins.PluginInterface):
|
||||
if self.config["dump"]:
|
||||
file_output = self.dump_module(session_layers, pe_table_name, mod)
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(mod.vol.offset),
|
||||
format_hints.Hex(mod.DllBase),
|
||||
format_hints.Hex(mod.SizeOfImage),
|
||||
BaseDllName,
|
||||
FullDllName,
|
||||
file_output,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(mod.vol.offset),
|
||||
format_hints.Hex(mod.DllBase),
|
||||
format_hints.Hex(mod.SizeOfImage),
|
||||
BaseDllName,
|
||||
FullDllName,
|
||||
file_output,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -436,7 +436,6 @@ class PoolScanner(plugins.PluginInterface):
|
||||
constraints,
|
||||
alignment=alignment,
|
||||
):
|
||||
|
||||
mem_objects = header.get_object(
|
||||
constraint=constraint,
|
||||
use_top_down=is_windows_8_or_later,
|
||||
|
||||
@@ -246,13 +246,29 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]:
|
||||
for _, entry in self._generator():
|
||||
if isinstance(entry.last_modify_time, datetime.datetime):
|
||||
yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time
|
||||
yield (
|
||||
f"Amcache: {entry.entry_type} {entry.path} registry key modified",
|
||||
timeliner.TimeLinerType.MODIFIED,
|
||||
entry.last_modify_time,
|
||||
)
|
||||
if isinstance(entry.last_modify_time_2, datetime.datetime):
|
||||
yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2
|
||||
yield (
|
||||
f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time",
|
||||
timeliner.TimeLinerType.CREATED,
|
||||
entry.last_modify_time_2,
|
||||
)
|
||||
if isinstance(entry.install_time, datetime.datetime):
|
||||
yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time
|
||||
yield (
|
||||
f"Amcache: {entry.entry_type} {entry.path} installed",
|
||||
timeliner.TimeLinerType.CREATED,
|
||||
entry.install_time,
|
||||
)
|
||||
if isinstance(entry.compile_time, datetime.datetime):
|
||||
yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time
|
||||
yield (
|
||||
f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)",
|
||||
timeliner.TimeLinerType.MODIFIED,
|
||||
entry.compile_time,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_amcache_hive(
|
||||
@@ -319,20 +335,23 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
vollog.debug(f"Found sha1hash {sha1_hash}")
|
||||
product_name = _get_string_value(values, val_enum.Product.value)
|
||||
|
||||
yield program_id, _AmcacheEntry(
|
||||
AmcacheEntryType.File.name,
|
||||
path=path,
|
||||
company=company,
|
||||
last_modify_time=last_mod_time,
|
||||
last_modify_time_2=last_mod_time_2,
|
||||
install_time=install_time,
|
||||
compile_time=compile_time,
|
||||
sha1_hash=(
|
||||
sha1_hash.lstrip("0000")
|
||||
if isinstance(sha1_hash, str)
|
||||
else sha1_hash
|
||||
yield (
|
||||
program_id,
|
||||
_AmcacheEntry(
|
||||
AmcacheEntryType.File.name,
|
||||
path=path,
|
||||
company=company,
|
||||
last_modify_time=last_mod_time,
|
||||
last_modify_time_2=last_mod_time_2,
|
||||
install_time=install_time,
|
||||
compile_time=compile_time,
|
||||
sha1_hash=(
|
||||
sha1_hash.lstrip("0000")
|
||||
if isinstance(sha1_hash, str)
|
||||
else sha1_hash
|
||||
),
|
||||
product_name=product_name,
|
||||
),
|
||||
product_name=product_name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -365,15 +384,18 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
)
|
||||
version = _get_string_value(values, val_enum.Version.value)
|
||||
|
||||
yield program_id, _AmcacheEntry(
|
||||
AmcacheEntryType.Program.name,
|
||||
company=company,
|
||||
last_modify_time=conversion.wintime_to_datetime(
|
||||
program_key.LastWriteTime.QuadPart
|
||||
yield (
|
||||
program_id,
|
||||
_AmcacheEntry(
|
||||
AmcacheEntryType.Program.name,
|
||||
company=company,
|
||||
last_modify_time=conversion.wintime_to_datetime(
|
||||
program_key.LastWriteTime.QuadPart
|
||||
),
|
||||
install_time=install_time,
|
||||
product_name=product,
|
||||
product_version=version,
|
||||
),
|
||||
install_time=install_time,
|
||||
product_name=product,
|
||||
product_version=version,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -411,14 +433,17 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
|
||||
product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore
|
||||
|
||||
yield program_id.strip().strip("\u0000"), _AmcacheEntry(
|
||||
AmcacheEntryType.Program.name,
|
||||
path=path,
|
||||
last_modify_time=last_mod,
|
||||
install_time=install_date,
|
||||
product_name=product,
|
||||
company=publisher,
|
||||
product_version=version,
|
||||
yield (
|
||||
program_id.strip().strip("\u0000"),
|
||||
_AmcacheEntry(
|
||||
AmcacheEntryType.Program.name,
|
||||
path=path,
|
||||
last_modify_time=last_mod,
|
||||
install_time=install_date,
|
||||
product_name=product,
|
||||
company=publisher,
|
||||
product_version=version,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -456,19 +481,22 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
prod_ver = _get_string_value(values, val_enum.ProductVersion.value)
|
||||
program_id = _get_string_value(values, val_enum.ProgramID.value)
|
||||
|
||||
yield program_id, _AmcacheEntry(
|
||||
AmcacheEntryType.File.name,
|
||||
path=path,
|
||||
company=publisher,
|
||||
last_modify_time=last_mod,
|
||||
compile_time=linkdate,
|
||||
sha1_hash=(
|
||||
sha1_hash.lstrip("0000")
|
||||
if isinstance(sha1_hash, str)
|
||||
else sha1_hash
|
||||
yield (
|
||||
program_id,
|
||||
_AmcacheEntry(
|
||||
AmcacheEntryType.File.name,
|
||||
path=path,
|
||||
company=publisher,
|
||||
last_modify_time=last_mod,
|
||||
compile_time=linkdate,
|
||||
sha1_hash=(
|
||||
sha1_hash.lstrip("0000")
|
||||
if isinstance(sha1_hash, str)
|
||||
else sha1_hash
|
||||
),
|
||||
product_name=prod_name,
|
||||
product_version=prod_ver,
|
||||
),
|
||||
product_name=prod_name,
|
||||
product_version=prod_ver,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -485,7 +513,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
wanted_values = [key.value for key in val_enum]
|
||||
|
||||
for binary_key in driver_binary_key.get_subkeys():
|
||||
|
||||
values = {
|
||||
str(value.get_name()): value
|
||||
for value in binary_key.get_values()
|
||||
@@ -636,7 +663,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
yield 0, empty_program
|
||||
|
||||
def run(self):
|
||||
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("EntryType", str),
|
||||
|
||||
@@ -185,7 +185,6 @@ NULL = "\u0000"
|
||||
|
||||
|
||||
class _ScheduledTasksReader(io.BytesIO):
|
||||
|
||||
def read_task_scheduler_time(self) -> Optional[datetime.datetime]:
|
||||
_ = bool(self.read_aligned_u1()) # is_localized
|
||||
filetime = self.decode_filetime()
|
||||
@@ -393,7 +392,6 @@ class TaskAction:
|
||||
|
||||
num_attachment_filenames = reader.read_u4()
|
||||
if num_attachment_filenames is not None:
|
||||
|
||||
attachment_filenames = [
|
||||
reader.read_bstring() for _ in range(num_attachment_filenames)
|
||||
]
|
||||
@@ -1138,11 +1136,23 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte
|
||||
) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]:
|
||||
for _, task in self._generator():
|
||||
if isinstance(task.last_run_time, datetime.datetime):
|
||||
yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", timeliner.TimeLinerType.ACCESSED, task.last_run_time
|
||||
yield (
|
||||
f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran",
|
||||
timeliner.TimeLinerType.ACCESSED,
|
||||
task.last_run_time,
|
||||
)
|
||||
if isinstance(task.last_successful_run_time, datetime.datetime):
|
||||
yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", timeliner.TimeLinerType.ACCESSED, task.last_successful_run_time
|
||||
yield (
|
||||
f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully",
|
||||
timeliner.TimeLinerType.ACCESSED,
|
||||
task.last_successful_run_time,
|
||||
)
|
||||
if isinstance(task.creation_time, datetime.datetime):
|
||||
yield f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or '<UNKNOWN>'}", timeliner.TimeLinerType.CREATED, task.creation_time
|
||||
yield (
|
||||
f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or '<UNKNOWN>'}",
|
||||
timeliner.TimeLinerType.CREATED,
|
||||
task.creation_time,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_software_hive(
|
||||
@@ -1203,7 +1213,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte
|
||||
def parse_dynamic_info_value(
|
||||
cls, dyn_info_value: reg_extensions.CM_KEY_VALUE
|
||||
) -> Optional[DynamicInfo]:
|
||||
|
||||
try:
|
||||
data = dyn_info_value.decode_data()
|
||||
except exceptions.InvalidAddressException:
|
||||
@@ -1318,7 +1327,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte
|
||||
all_actions = action_set.actions or [None] if action_set is not None else [None]
|
||||
|
||||
for action, trigger in itertools.product(all_actions, all_triggers):
|
||||
|
||||
if action is not None:
|
||||
if action.action_type in (
|
||||
ActionType.Exe,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import logging
|
||||
from volatility3.framework import interfaces, deprecation
|
||||
|
||||
from volatility3.framework import deprecation, interfaces
|
||||
from volatility3.plugins.windows.registry import scheduled_tasks
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -12,7 +13,7 @@ class ScheduledTasks(
|
||||
interfaces.plugins.PluginInterface,
|
||||
deprecation.PluginRenameClass,
|
||||
replacement_class=scheduled_tasks.ScheduledTasks,
|
||||
removal_date="2025-09-25",
|
||||
removal_date="2026-09-25",
|
||||
):
|
||||
"""Decodes scheduled task information from the Windows registry, including
|
||||
information about triggers, actions, run times, and creation times (deprecated)."""
|
||||
|
||||
@@ -95,13 +95,16 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
|
||||
# Group and yield each row
|
||||
for rows in sessions.values():
|
||||
for row in rows:
|
||||
yield 0, (
|
||||
row.get("session_id"),
|
||||
row.get("session_type"),
|
||||
row.get("process_id"),
|
||||
row.get("process_name"),
|
||||
row.get("user_name"),
|
||||
row.get("process_start"),
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
row.get("session_id"),
|
||||
row.get("session_type"),
|
||||
row.get("process_id"),
|
||||
row.get("process_name"),
|
||||
row.get("user_name"),
|
||||
row.get("process_start"),
|
||||
),
|
||||
)
|
||||
|
||||
def generate_timeline(self):
|
||||
|
||||
@@ -51,9 +51,17 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf
|
||||
) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime]]:
|
||||
for _, (_, last_modified, last_update, _, _, file_path) in self._generator():
|
||||
if isinstance(last_update, datetime):
|
||||
yield f"Shimcache: File {file_path} executed", timeliner.TimeLinerType.ACCESSED, last_update
|
||||
yield (
|
||||
f"Shimcache: File {file_path} executed",
|
||||
timeliner.TimeLinerType.ACCESSED,
|
||||
last_update,
|
||||
)
|
||||
if isinstance(last_modified, datetime):
|
||||
yield f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified
|
||||
yield (
|
||||
f"Shimcache: File {file_path} modified",
|
||||
timeliner.TimeLinerType.MODIFIED,
|
||||
last_modified,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -161,7 +169,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf
|
||||
continue
|
||||
|
||||
try:
|
||||
if proc_layer.read(vad.get_start(), 4) != b"\xEF\xBE\xAD\xDE":
|
||||
if proc_layer.read(vad.get_start(), 4) != b"\xef\xbe\xad\xde":
|
||||
if pid == 624:
|
||||
vollog.debug("VAD magic bytes don't match DEADBEEF")
|
||||
continue
|
||||
|
||||
@@ -150,7 +150,6 @@ class SvcScan(interfaces.plugins.PluginInterface):
|
||||
def _get_service_key(
|
||||
context, config_path: str, kernel_module_name: str
|
||||
) -> Optional[objects.StructType]:
|
||||
|
||||
for hive in hivelist.HiveList.list_hives(
|
||||
context=context,
|
||||
base_config_path=interfaces.configuration.path_join(
|
||||
|
||||
@@ -167,16 +167,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
|
||||
info = self.gather_thread_info(ethread, vads_cache)
|
||||
|
||||
if info:
|
||||
yield 0, (
|
||||
format_hints.Hex(info.offset),
|
||||
info.pid,
|
||||
info.tid,
|
||||
format_hints.Hex(info.start_addr),
|
||||
info.start_path or renderers.NotAvailableValue(),
|
||||
format_hints.Hex(info.win32_start_addr),
|
||||
info.win32_start_path or renderers.NotAvailableValue(),
|
||||
info.create_time,
|
||||
info.exit_time,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(info.offset),
|
||||
info.pid,
|
||||
info.tid,
|
||||
format_hints.Hex(info.start_addr),
|
||||
info.start_path or renderers.NotAvailableValue(),
|
||||
format_hints.Hex(info.win32_start_addr),
|
||||
info.win32_start_path or renderers.NotAvailableValue(),
|
||||
info.create_time,
|
||||
info.exit_time,
|
||||
),
|
||||
)
|
||||
|
||||
def generate_timeline(self):
|
||||
|
||||
@@ -62,7 +62,6 @@ class VadRegExScan(plugins.PluginInterface):
|
||||
vollog.debug(f"RegEx Pattern: {regex_pattern}")
|
||||
|
||||
for proc in procs:
|
||||
|
||||
# attempt to create a process layer for each proc
|
||||
proc_layer_name = proc.add_process_layer()
|
||||
if not proc_layer_name:
|
||||
@@ -106,12 +105,15 @@ class VadRegExScan(plugins.PluginInterface):
|
||||
max_length=proc.ImageFileName.vol.count,
|
||||
errors="replace",
|
||||
)
|
||||
yield 0, (
|
||||
proc_id,
|
||||
process_name,
|
||||
format_hints.Hex(offset),
|
||||
text_result,
|
||||
bytes_result,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
proc_id,
|
||||
process_name,
|
||||
format_hints.Hex(offset),
|
||||
text_result,
|
||||
bytes_result,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import logging
|
||||
from typing import Iterable, List, Tuple
|
||||
import datetime
|
||||
|
||||
from volatility3.framework import interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
@@ -18,7 +19,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
"""Scans all the Virtual Address Descriptor memory maps using yara."""
|
||||
|
||||
_required_framework_version = (2, 22, 0)
|
||||
_version = (1, 1, 3)
|
||||
_version = (1, 1, 4)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -99,12 +100,24 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
layer_name=layer.name,
|
||||
length=len(value),
|
||||
)
|
||||
yield 0, (
|
||||
format_hints.Hex(offset),
|
||||
task.UniqueProcessId,
|
||||
rule_name,
|
||||
name,
|
||||
layer_data,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(offset),
|
||||
task.UniqueProcessId,
|
||||
task.get_create_time(),
|
||||
task.InheritedFromUniqueProcessId,
|
||||
task.ImageFileName.cast(
|
||||
"string",
|
||||
max_length=task.ImageFileName.vol.count,
|
||||
errors="replace",
|
||||
),
|
||||
task.get_session_id(),
|
||||
task.ActiveThreads,
|
||||
rule_name,
|
||||
name,
|
||||
layer_data,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -130,6 +143,11 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
|
||||
[
|
||||
("Offset", format_hints.Hex),
|
||||
("PID", int),
|
||||
("CreateTime", datetime.datetime),
|
||||
("PPID", int),
|
||||
("ImageFileName", str),
|
||||
("SessionId", int),
|
||||
("Threads", int),
|
||||
("Rule", str),
|
||||
("Component", str),
|
||||
("Value", renderers.LayerData),
|
||||
|
||||
@@ -112,15 +112,18 @@ class Windows(interfaces.plugins.PluginInterface):
|
||||
)
|
||||
continue
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(window.vol.offset),
|
||||
station_name,
|
||||
sess_id,
|
||||
desktop_name,
|
||||
window_name or renderers.NotAvailableValue(),
|
||||
window_proc,
|
||||
process_name,
|
||||
process_pid,
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(window.vol.offset),
|
||||
station_name,
|
||||
sess_id,
|
||||
desktop_name,
|
||||
window_name or renderers.NotAvailableValue(),
|
||||
window_proc,
|
||||
process_name,
|
||||
process_pid,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
Renderers display the unified output format in some manner (be it text
|
||||
or file or graphical output
|
||||
"""
|
||||
|
||||
import collections
|
||||
import collections.abc
|
||||
import dataclasses
|
||||
|
||||
@@ -8,6 +8,7 @@ These hints allow a plugin to indicate how they would like data from a particula
|
||||
|
||||
Text renderers should attempt to honour all hints provided in this module where possible
|
||||
"""
|
||||
|
||||
from typing import Type, Union
|
||||
|
||||
from volatility3.framework import interfaces
|
||||
|
||||
@@ -210,9 +210,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface):
|
||||
replacements = set()
|
||||
# Whole Symbols that still need traversing
|
||||
while traverse_list:
|
||||
template_traverse_list, traverse_list = [
|
||||
self._resolved[traverse_list[0]]
|
||||
], traverse_list[1:]
|
||||
template_traverse_list, traverse_list = (
|
||||
[self._resolved[traverse_list[0]]],
|
||||
traverse_list[1:],
|
||||
)
|
||||
# Traverse a single symbol looking for any ReferenceTemplate objects
|
||||
while template_traverse_list:
|
||||
traverser, template_traverse_list = (
|
||||
|
||||
@@ -246,9 +246,12 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface):
|
||||
if name.endswith(zip_match + extension) or (
|
||||
zip_match == "*" and name.endswith(extension)
|
||||
):
|
||||
yield "jar:file:" + str(
|
||||
pathlib.Path(zip_path)
|
||||
) + "!" + name
|
||||
yield (
|
||||
"jar:file:"
|
||||
+ str(pathlib.Path(zip_path))
|
||||
+ "!"
|
||||
+ name
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
|
||||
@@ -99,8 +99,10 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
"""Class with multiple useful linux functions."""
|
||||
|
||||
_version = (2, 3, 1)
|
||||
_version = (2, 4, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
deleted = "(deleted)"
|
||||
smear = "<potentially smeared>"
|
||||
|
||||
framework.require_interface_version(*_required_framework_version)
|
||||
|
||||
@@ -168,6 +170,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
# vfsmnt can be the vfsmount object itself (>=3.3) or a vfsmount * (<3.3)
|
||||
return ""
|
||||
|
||||
inode = dentry.d_inode
|
||||
path_reversed = []
|
||||
smeared = False
|
||||
while (
|
||||
@@ -191,6 +194,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
|
||||
parent = dentry.d_parent
|
||||
dname = dentry.d_name.name_as_str()
|
||||
|
||||
# empty dentry names are most likely
|
||||
# the result of smearing
|
||||
if not dname:
|
||||
@@ -203,7 +207,10 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
# if there is smear the missing dname will be empty. e.g. if the normal
|
||||
# path would be /foo/bar/baz, but bar is missing due to smear the results
|
||||
# returned here will show /foo//baz. Note the // for the missing dname.
|
||||
return f"<potentially smeared> {path}"
|
||||
return f"{LinuxUtilities.smear} {path}"
|
||||
|
||||
if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0:
|
||||
path = f" {path} {LinuxUtilities.deleted}"
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
@@ -301,7 +308,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
return f"{pre_name}:[{inode.i_ino:d}]"
|
||||
|
||||
@classmethod
|
||||
def path_for_file(cls, context, task, filp) -> str:
|
||||
def path_for_file(cls, context, task, filp, files_only=False) -> str:
|
||||
"""Returns a file (or sock pipe) pathname relative to the task's root directory.
|
||||
|
||||
A 'file' structure doesn't have enough information to properly restore its
|
||||
@@ -340,7 +347,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
except exceptions.InvalidAddressException:
|
||||
dname_is_valid = False
|
||||
|
||||
if dname_is_valid:
|
||||
if dname_is_valid and not files_only:
|
||||
ret = LinuxUtilities._get_new_sock_pipe_path(context, task, filp)
|
||||
else:
|
||||
ret = LinuxUtilities._get_path_file(task, filp)
|
||||
@@ -353,6 +360,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
context: interfaces.context.ContextInterface,
|
||||
symbol_table: str,
|
||||
task: interfaces.objects.ObjectInterface,
|
||||
files_only: bool = False,
|
||||
):
|
||||
try:
|
||||
files = task.files
|
||||
@@ -376,7 +384,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
|
||||
for fd_num, filp in enumerate(fds):
|
||||
if filp and filp.is_readable():
|
||||
full_path = LinuxUtilities.path_for_file(context, task, filp)
|
||||
full_path = LinuxUtilities.path_for_file(
|
||||
context, task, filp, files_only
|
||||
)
|
||||
|
||||
yield fd_num, filp, full_path
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class module(generic.GenericIntelProcess):
|
||||
|
||||
def is_valid(self):
|
||||
"""Determine whether it is a valid module object by verifying the self-referential
|
||||
in module_kobject. This also confirms that the module is actively allocated and
|
||||
@@ -991,7 +990,6 @@ class maple_tree(objects.StructType):
|
||||
|
||||
|
||||
class mm_struct(objects.StructType):
|
||||
|
||||
# TODO: As of version 3.0.0 this method should be removed
|
||||
def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""
|
||||
@@ -1273,6 +1271,42 @@ class vm_area_struct(objects.StructType):
|
||||
except exceptions.InvalidAddressException:
|
||||
return None
|
||||
|
||||
def get_malicious_pages(self, proclayer) -> List[int]:
|
||||
"""Identifies and returns a list of potentially malicious memory pages.
|
||||
|
||||
A page is considered malicious if it is:
|
||||
- Executable (protection flags match 'r-x')
|
||||
- Dirty (modified since process start, according to proclayer.is_dirty())
|
||||
|
||||
Args:
|
||||
proclayer: The process's memory layer
|
||||
|
||||
Returns:
|
||||
List[int]: A list of virtual addresses for pages flagged as potentially malicious.
|
||||
"""
|
||||
|
||||
malicious_pages = []
|
||||
flags_str = self.get_protection()
|
||||
|
||||
if (
|
||||
proclayer
|
||||
and "r-x" in flags_str
|
||||
and self.vm_file.dereference().vol.offset != 0
|
||||
):
|
||||
for i in range(self.vm_start, self.vm_end, proclayer.page_size):
|
||||
try:
|
||||
if proclayer.is_dirty(i):
|
||||
vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !")
|
||||
malicious_pages.append(i)
|
||||
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
|
||||
break
|
||||
return malicious_pages
|
||||
|
||||
# used by malfind
|
||||
def is_suspicious(self, proclayer=None):
|
||||
ret = False
|
||||
@@ -1288,7 +1322,7 @@ class vm_area_struct(objects.StructType):
|
||||
try:
|
||||
if proclayer.is_dirty(i):
|
||||
vollog.warning(
|
||||
f"Found malicious (dirty+exec) page at {hex(i)} !"
|
||||
f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !"
|
||||
)
|
||||
# We do not attempt to find other dirty+exec pages once we have found one
|
||||
ret = True
|
||||
@@ -1971,8 +2005,9 @@ class mnt_namespace(objects.StructType):
|
||||
self._context, self
|
||||
)
|
||||
for node in self.mounts.get_nodes():
|
||||
# See kernel's node_to_mount()
|
||||
mnt = linux.LinuxUtilities.container_of(
|
||||
node, "mount", "mnt_list", vmlinux
|
||||
node, "mount", "mnt_node", vmlinux
|
||||
)
|
||||
yield mnt
|
||||
else:
|
||||
@@ -3048,7 +3083,6 @@ class latch_tree_root(objects.StructType):
|
||||
|
||||
|
||||
class kernel_symbol(objects.StructType):
|
||||
|
||||
def _offset_to_ptr(self, off) -> int:
|
||||
layer = self._context.layers[self.vol.layer_name]
|
||||
long_mask = (1 << layer.bits_per_register) - 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user