Merge branch 'volatilityfoundation:develop' into linux_sockscan

This commit is contained in:
Eve
2024-09-27 10:35:18 +01:00
committed by GitHub
121 changed files with 14780 additions and 855 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check --diff --verbose"
+6 -7
View File
@@ -18,26 +18,25 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
python-version: ["3.8"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
pip install build
- name: Build PyPi packages
run: |
python setup.py sdist --formats=gztar,zip
python setup.py bdist_wheel
python -m build
- name: Archive dist
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: volatility3-pypi
path: |
+2 -2
View File
@@ -8,9 +8,9 @@ jobs:
fail-fast: false
matrix:
host: [ ubuntu-latest, windows-latest ]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
+6 -7
View File
@@ -6,11 +6,11 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
python-version: ["3.8"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
@@ -18,13 +18,12 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install Cmake
pip install setuptools wheel
pip install build
pip install -r ./test/requirements-testing.txt
- name: Build PyPi packages
run: |
python setup.py sdist --formats=gztar,zip
python setup.py bdist_wheel
python -m build
- name: Download images
run: |
@@ -47,7 +46,7 @@ jobs:
- name: Clean up post-test
run: |
rm -rf *.lime
rm -rf *.bin
rm -rf *.img
cd volatility3/symbols
rm -rf linux
+1
View File
@@ -27,6 +27,7 @@ config*.json
# Pyinstaller files
build
dist
*.egg-info
# Environments
.env
+1 -1
View File
@@ -14,7 +14,7 @@ authors:
identifiers:
- type: url
value: 'https://github.com/volatilityfoundation/volatility3'
description: Volatility 3 source code respository
description: Volatility 3 source code repository
repository-code: 'https://github.com/volatilityfoundation/volatility3'
url: 'https://github.com/volatilityfoundation/volatility3'
abstract: >-
+3 -4
View File
@@ -20,17 +20,16 @@ more details.
## Requirements
Volatility 3 requires Python 3.7.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
Volatility 3 requires Python 3.8.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
```shell
pip3 install -r requirements-minimal.txt
```
Alternately, the minimal packages will be installed automatically when Volatility 3 is installed using setup.py. However, as noted in the Quick Start section below, Volatility 3 does not *need* to be installed via setup.py prior to using it.
Alternately, the minimal packages will be installed automatically when Volatility 3 is installed using pip. However, as noted in the Quick Start section below, Volatility 3 does not *need* to be installed prior to using it.
```shell
python3 setup.py build
python3 setup.py install
pip3 install .
```
To enable the full range of Volatility 3 functionality, use a command like the one below. For partial functionality, comment out any unnecessary packages in [requirements.txt](requirements.txt) prior to running the command.
+1
View File
@@ -4,5 +4,6 @@ sphinx_autodoc_typehints>=1.4.0
sphinx-rtd-theme>=0.4.3
yara-python
yara-x
pycryptodome
pefile
+32
View File
@@ -0,0 +1,32 @@
[project]
name = "volatility3"
description = "Memory forensics framework"
keywords = ["volatility", "memory", "forensics", "framework", "windows", "linux", "volshell"]
readme = "README.md"
authors = [
{ name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" },
]
requires-python = ">=3.8.0"
license = { text = "VSL" }
dynamic = ["dependencies", "optional-dependencies", "version"]
[project.urls]
Homepage = "https://github.com/volatilityfoundation/volatility3/"
"Bug Tracker" = "https://github.com/volatilityfoundation/volatility3/issues"
Documentation = "https://volatility3.readthedocs.io/"
"Source Code" = "https://github.com/volatilityfoundation/volatility3"
[project.scripts]
vol = "volatility3.cli:main"
volshell = "volatility3.cli.volshell:main"
[tool.setuptools.dynamic]
version = { attr = "volatility3.framework.constants._version.PACKAGE_VERSION" }
dependencies = { file = "requirements-minimal.txt" }
[tool.setuptools.packages.find]
include = ["volatility3*"]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
+4 -17
View File
@@ -1,22 +1,9 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
# This is required for the yara plugins
yara-python>=3.8.0
# This is required for several plugins that perform malware analysis and disassemble code.
# It can also improve accuracy of Windows 8 and later memory samples.
capstone>=3.0.5
# This is required by plugins that decrypt passwords, password hashes, etc.
pycryptodome
-r requirements.txt
# This can improve error messages regarding improperly configured ISF files,
# but is only recommended for development
jsonschema>=2.3.0
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
# Used to build executable file
pyinstaller>=6.5.0
pyinstaller-hooks-contrib>=2024.3
+3 -3
View File
@@ -1,5 +1,5 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# Include the minimal requirements
-r requirements-minimal.txt
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
@@ -15,7 +15,7 @@ capstone>=3.0.5
pycryptodome
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
leechcorepyc>=2.4.0; sys_platform != 'darwin'
# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage
gcsfs>=2023.1.0
+6 -35
View File
@@ -4,50 +4,21 @@
import setuptools
from volatility3.framework import constants
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
def get_install_requires():
def get_requires(filename):
requirements = []
with open("requirements-minimal.txt", "r", encoding="utf-8") as fh:
with open(filename, "r", encoding="utf-8") as fh:
for line in fh.readlines():
stripped_line = line.strip()
if stripped_line == "" or stripped_line.startswith("#"):
if stripped_line == "" or stripped_line.startswith(("#", "-r")):
continue
requirements.append(stripped_line)
return requirements
setuptools.setup(
name="volatility3",
description="Memory forensics framework",
version=constants.PACKAGE_VERSION,
license="VSL",
keywords="volatility memory forensics framework windows linux volshell",
author="Volatility Foundation",
long_description=long_description,
long_description_content_type="text/markdown",
author_email="volatility@volatilityfoundation.org",
url="https://github.com/volatilityfoundation/volatility3/",
project_urls={
"Bug Tracker": "https://github.com/volatilityfoundation/volatility3/issues",
"Documentation": "https://volatility3.readthedocs.io/",
"Source Code": "https://github.com/volatilityfoundation/volatility3",
extras_require={
"dev": get_requires("requirements-dev.txt"),
"full": get_requires("requirements.txt"),
},
packages=setuptools.find_namespace_packages(
include=["volatility3", "volatility3.*"]
),
package_dir={"volatility3": "volatility3"},
python_requires=">=3.7.0",
include_package_data=True,
entry_points={
"console_scripts": [
"vol = volatility3.cli:main",
"volshell = volatility3.cli.volshell:main",
],
},
install_requires=get_install_requires(),
)
+1
View File
@@ -6,5 +6,6 @@ pefile>=2017.8.1 #foo
# This is required for the yara plugins
yara-python>=3.8.0
yara-x>=0.5.0
pytest>=7.0.0
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# 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
+25 -8
View File
@@ -22,6 +22,13 @@ import traceback
from typing import Any, Dict, List, Tuple, Type, Union
from urllib import parse, request
try:
import argcomplete
HAS_ARGCOMPLETE = True
except ImportError:
HAS_ARGCOMPLETE = False
from volatility3.cli import text_filter
import volatility3.plugins
import volatility3.symbols
@@ -351,6 +358,10 @@ class CommandLine:
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
# Run the argparser
if HAS_ARGCOMPLETE:
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
# before all the plugins have been added
argcomplete.autocomplete(parser)
args = parser.parse_args()
if args.plugin is None:
parser.error("Please select a plugin to run")
@@ -716,19 +727,17 @@ class CommandLine:
"""Gets the final filename"""
if output_dir is None:
raise TypeError("Output directory is not a string")
os.makedirs(output_dir, exist_ok=True)
pref_name_array = self.preferred_filename.split(".")
filename, extension = (
os.path.join(output_dir, ".".join(pref_name_array[:-1])),
pref_name_array[-1],
)
output_filename = f"{filename}.{extension}"
output_filename = os.path.join(output_dir, self.preferred_filename)
filename, extension = os.path.splitext(output_filename)
counter = 1
while os.path.exists(output_filename):
output_filename = f"{filename}-{counter}.{extension}"
output_filename = f"{filename}-{counter}{extension}"
counter += 1
return output_filename
class CLIMemFileHandler(io.BytesIO, CLIFileHandler):
@@ -791,8 +800,16 @@ class CommandLine:
if self._file.closed:
return None
self._file.close()
output_filename = self._get_final_filename()
# Update the filename, which may have changed if a file with
# the same name already existed. This needs to be done before
# closing the file, otherwise FileHandlerInterface will raise
# an exception. Also, the preferred_filename setter only allows
# a specific set of characters, where '/' is not in that list
self.preferred_filename = os.path.basename(output_filename)
self._file.close()
os.rename(self._name, output_filename)
if direct:
+49 -33
View File
@@ -25,7 +25,7 @@ except ImportError:
vollog.debug("Disassembly library capstone not found")
def hex_bytes_as_text(value: bytes) -> str:
def hex_bytes_as_text(value: bytes, width: int = 16) -> str:
"""Renders HexBytes as text.
Args:
@@ -36,19 +36,24 @@ def hex_bytes_as_text(value: bytes) -> str:
"""
if not isinstance(value, bytes):
raise TypeError(f"hex_bytes_as_text takes bytes not: {type(value)}")
ascii = []
hex = []
count = 0
output = ""
for byte in value:
hex.append(f"{byte:02x}")
ascii.append(chr(byte) if 0x20 < byte <= 0x7E else ".")
if (count % 8) == 7:
output += "\n"
output += " ".join(hex[count - 7 : count + 1])
output += "\t"
output += "".join(ascii[count - 7 : count + 1])
count += 1
printables = ""
output = "\n"
for count, byte in enumerate(value):
output += f"{byte:02x} "
char = chr(byte)
printables += char if 0x20 <= byte <= 0x7E else "."
if count % width == width - 1:
output += printables
if count < len(value) - 1:
output += "\n"
printables = ""
# Handle leftovers when the lenght is not mutiple of width
if printables:
output += " " * (width - len(printables))
output += printables
return output
@@ -174,7 +179,14 @@ class QuickTextRenderer(CLIRenderer):
outfd.write("\n{}\n".format("\t".join(line)))
def visitor(node: interfaces.renderers.TreeNode, accumulator):
if self.filter and self.filter.filter(node.values):
line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
line.append(renderer(node.values[column_index]))
if self.filter and self.filter.filter(line):
return accumulator
accumulator.write("\n")
@@ -183,13 +195,6 @@ class QuickTextRenderer(CLIRenderer):
"*" * max(0, node.path_depth - 1)
+ ("" if (node.path_depth <= 1) else " ")
)
line = []
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
line.append(renderer(node.values[column_index]))
accumulator.write("{}".format("\t".join(line)))
accumulator.flush()
return accumulator
@@ -254,12 +259,17 @@ class CSVRenderer(CLIRenderer):
def visitor(node: interfaces.renderers.TreeNode, accumulator):
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
row = {"TreeDepth": str(max(0, node.path_depth - 1))}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
row[f"{column.name}"] = renderer(node.values[column_index])
line.append(row[f"{column.name}"])
if self.filter and self.filter.filter(line):
return accumulator
accumulator.writerow(row)
return accumulator
@@ -312,12 +322,9 @@ class PrettyTextRenderer(CLIRenderer):
max_column_widths.get(tree_indent_column, 0), node.path_depth
)
if self.filter and self.filter.filter(node.values):
return accumulator
line = {}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
rendered_line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
@@ -329,6 +336,11 @@ class PrettyTextRenderer(CLIRenderer):
max_column_widths.get(column.name, len(column.name)), field_width
)
line[column] = data.split("\n")
rendered_line.append(data)
if self.filter and self.filter.filter(rendered_line):
return accumulator
accumulator.append((node.path_depth, line))
return accumulator
@@ -342,8 +354,7 @@ class PrettyTextRenderer(CLIRenderer):
format_string_list = [
"{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"
]
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
for column_index, column in enumerate(grid.columns):
format_string_list.append(
"{"
+ str(column_index + 1)
@@ -432,8 +443,8 @@ class JsonRenderer(CLIRenderer):
# 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": []}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
@@ -441,6 +452,11 @@ class JsonRenderer(CLIRenderer):
if isinstance(data, interfaces.renderers.BaseAbsentValue):
data = None
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)
else:
+17 -2
View File
@@ -21,8 +21,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__
self.choices = None
def __call__(
self,
@@ -100,3 +98,20 @@ class HelpfulArgParser(argparse.ArgumentParser):
# return the number of arguments matched
return len(match.group(1))
def _check_value(self, action: argparse.Action, value: Any) -> None:
"""This is called to ensure a value is correct/valid
In normal operation, it would check that a value provided is valid and return None
If it was not valid, it would throw an ArgumentError
When people provide a partial plugin name, we want to look for a matching plugin name
which happens in the HelpfulSubparserAction's __call_method
To get there without tripping the check_value failure, we have to prevent the exception
being thrown when the value is a HelpfulSubparserAction. This therefore affects no other
checks for normal parameters.
"""
if not isinstance(action, HelpfulSubparserAction):
super()._check_value(action, value)
return None
+12
View File
@@ -21,6 +21,14 @@ from volatility3.framework import (
plugins,
)
try:
import argcomplete
HAS_ARGCOMPLETE = True
except ImportError:
HAS_ARGCOMPLETE = False
# Make sure we log everything
rootlog = logging.getLogger()
@@ -276,6 +284,10 @@ class VolShell(cli.CommandLine):
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
# Run the argparser
if HAS_ARGCOMPLETE:
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
# before all the plugins have been added
argcomplete.autocomplete(parser)
args = parser.parse_args()
vollog.log(
+8 -2
View File
@@ -7,7 +7,7 @@ import glob
import sys
import zipfile
required_python_version = (3, 7, 0)
required_python_version = (3, 8, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
@@ -223,8 +223,14 @@ def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
return plugin_list
def clear_cache(complete=False):
def clear_cache(complete=True):
try:
if complete:
glob_pattern = "*.cache"
for cache_filename in glob.glob(
os.path.join(constants.CACHE_PATH, glob_pattern)
):
os.unlink(cache_filename)
os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME))
except FileNotFoundError:
vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache")
+4 -1
View File
@@ -159,7 +159,10 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
# This we get for free
aslr_shift = (
init_task.files.cast("long unsigned int")
int.from_bytes(
init_task.files.cast("bytes", length=init_task.files.vol.size),
byteorder=init_task.files.vol.data_format.byteorder,
)
- module.get_symbol("init_files").address
)
kaslr_shift = init_task_address - cls.virtual_to_physical_address(
@@ -527,12 +527,14 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
if description is None:
description = f"Version {'.'.join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet"
super().__init__(
name=name, description=description, default=default, optional=optional
)
@@ -544,15 +546,51 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
self._version = version
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
self,
context: interfaces.context.ContextInterface,
config_path: str,
accumulator: Optional[
List[interfaces.configuration.VersionableInterface]
] = None,
) -> Dict[str, interfaces.configuration.RequirementInterface]:
# Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type
config_path = interfaces.configuration.path_join(config_path, self.name)
if not self.matches_required(self._version, self._component.version):
return {config_path: self}
recurse = True
if accumulator is None:
accumulator = set([self._component])
else:
if self._component in accumulator:
recurse = False
else:
accumulator.add(self._component)
# Check for child requirements
if (
issubclass(self._component, interfaces.configuration.ConfigurableInterface)
and recurse
):
result = {}
for requirement in self._component.get_requirements():
if not requirement.optional and isinstance(
requirement, VersionRequirement
):
result.update(
requirement.unsatisfied(
context, config_path, accumulator.copy()
)
)
if result:
result.update({config_path: self})
return result
context.config[interfaces.configuration.path_join(config_path, self.name)] = (
True
)
return {}
@classmethod
+9 -15
View File
@@ -14,6 +14,13 @@ from typing import Callable, Optional
import volatility3.framework.constants.linux
import volatility3.framework.constants.windows
from volatility3.framework.constants._version import (
PACKAGE_VERSION,
VERSION_MAJOR,
VERSION_MINOR,
VERSION_PATCH,
VERSION_SUFFIX,
)
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
@@ -42,20 +49,6 @@ if hasattr(sys, "frozen") and sys.frozen:
BANG = "!"
"""Constant used to delimit table names from type names when referring to a symbol"""
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 7 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
# TODO: At version 2.0.0, remove the symbol_shift feature
PACKAGE_VERSION = (
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
AUTOMAGIC_CONFIG_PATH = "automagic"
"""The root section within the context configuration for automagic values"""
@@ -141,4 +134,5 @@ def __getattr__(name):
]:
warnings.warn(f"{name} is deprecated", FutureWarning)
return globals()[f"{deprecated_tag}{name}"]
return None
return getattr(__import__(__name__), name)
@@ -0,0 +1,11 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 10 # 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 = (
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
@@ -0,0 +1,21 @@
from volatility3.framework.layers import intel
WIN_ARCHS = ["Intel32", "Intel64"]
"""Windows supported architectures"""
WIN_ARCHS_LAYERS = [intel.Intel]
"""Windows supported architectures layers"""
LINUX_ARCHS = ["Intel32", "Intel64"]
"""Linux supported architectures"""
LINUX_ARCHS_LAYERS = [intel.Intel]
"""Linux supported architectures layers"""
MAC_ARCHS = ["Intel32", "Intel64"]
"""Mac supported architectures"""
MAC_ARCHS_LAYERS = [intel.Intel]
"""Mac supported architectures layers"""
FRAMEWORK_ARCHS = ["Intel32", "Intel64"]
"""Framework supported architectures"""
FRAMEWORK_ARCHS_LAYERS = [intel.Intel]
"""Framework supported architectures layers"""
+5 -4
View File
@@ -256,12 +256,13 @@ class Module(interfaces.context.ModuleInterface):
if not absolute:
offset += self._offset
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if "layer_name" in kwargs:
del kwargs["layer_name"]
# We have to allow using an alternative layer name due to pool scanners switching
# to the memory layer for scanning samples prior to Windows 10.
layer_name = kwargs.pop("layer_name", self._layer_name)
return self._context.object(
object_type=object_type,
layer_name=self._layer_name,
layer_name=layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
**kwargs,
@@ -50,7 +50,7 @@ class AutomagicInterface(
context: interfaces.context.ContextInterface,
config_path: str,
*args,
**kwargs
**kwargs,
) -> None:
super().__init__(context, config_path)
for requirement in self.get_requirements():
@@ -494,8 +494,7 @@ class SimpleTypeRequirement(RequirementInterface):
"""Validates the instance requirement based upon its
`instance_type`."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
if not isinstance(value, self.instance_type):
vollog.log(
constants.LOGLEVEL_V,
@@ -536,7 +535,7 @@ class ClassRequirement(RequirementInterface):
"""Checks to see if a class can be recovered."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
self._cls = None
if value is not None and isinstance(value, str):
if "." in value:
+61 -43
View File
@@ -96,12 +96,13 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
def get_summary_header(self) -> interfaces.objects.ObjectInterface:
return self.context.object(
self._crash_common_table_name + constants.BANG + "_SUMMARY_DUMP",
offset=0x1000 * self.headerpages,
offset=self._page_size * self.headerpages,
layer_name=self._base_layer,
)
def _load_segments(self) -> None:
"""Loads up the segments from the meta_layer."""
"""Loads up the segments from the meta_layer.
A segment is a set of contiguous memory pages."""
segments = []
@@ -119,70 +120,87 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
for run in header.PhysicalMemoryBlockBuffer.Run:
segments.append(
(
run.BasePage * 0x1000,
offset * 0x1000,
run.PageCount * 0x1000,
run.PageCount * 0x1000,
run.BasePage * self._page_size,
offset * self._page_size,
run.PageCount * self._page_size,
run.PageCount * self._page_size,
)
)
offset += run.PageCount
elif self.dump_type == 0x05:
summary_header = self.get_summary_header()
first_bit = None # First bit in a run
first_offset = 0 # File offset of first bit
last_bit_seen = 0 # Most recent bit processed
offset = summary_header.HeaderSize # Size of file headers
buffer_char = summary_header.get_buffer_char()
buffer_long = summary_header.get_buffer_long()
for outer_index in range(0, ((summary_header.BitmapSize + 31) // 32)):
if buffer_long[outer_index] == 0:
if first_bit is not None:
last_bit = ((outer_index - 1) * 32) + 31
segment_length = (last_bit - first_bit + 1) * 0x1000
seg_first_bit = None # First bit in a run
seg_first_offset = 0 # File offset of first bit
offset = (
summary_header.HeaderSize
) # Offset to the start of actual memory dump
ulong_bitmap_array = summary_header.get_buffer_long()
# outer_index points to a 32 bits array inside a list of arrays,
# each bit indicating a page mapping state
for outer_index in range(0, ulong_bitmap_array.vol.count):
ulong_bitmap = ulong_bitmap_array[outer_index]
# All pages in this 32 bits array are mapped (speedup iteration process)
if ulong_bitmap == 0xFFFFFFFF:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = outer_index * 32
offset += 32 * self._page_size
# No pages in this 32 bits array are mapped (speedup iteration process)
elif ulong_bitmap == 0:
# End of segment
if seg_first_bit is not None:
last_bit = (outer_index - 1) * 32 + 31
segment_length = (
last_bit - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
elif buffer_long[outer_index] == 0xFFFFFFFF:
if first_bit is None:
first_offset = offset
first_bit = outer_index * 32
offset = offset + (32 * 0x1000)
seg_first_bit = None
# Some pages in this 32 bits array are mapped and some aren't
else:
for inner_index in range(0, 32):
bit_addr = outer_index * 32 + inner_index
if (buffer_char[bit_addr >> 3] >> (bit_addr & 0x7)) & 1:
if first_bit is None:
first_offset = offset
first_bit = bit_addr
offset = offset + 0x1000
for inner_bit_position in range(0, 32):
current_bit = outer_index * 32 + inner_bit_position
page_mapped = ulong_bitmap & (1 << inner_bit_position)
if page_mapped:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = current_bit
offset += self._page_size
else:
if first_bit is not None:
# End of segment
if seg_first_bit is not None:
segment_length = (
(bit_addr - 1) - first_bit + 1
) * 0x1000
current_bit - 1 - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
last_bit_seen = (outer_index * 32) + 31
seg_first_bit = None
last_bit_seen = outer_index * 32 + 31
if first_bit is not None:
segment_length = (last_bit_seen - first_bit + 1) * 0x1000
if seg_first_bit is not None:
segment_length = (last_bit_seen - seg_first_bit + 1) * self._page_size
segments.append(
(first_bit * 0x1000, first_offset, segment_length, segment_length)
(
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
else:
vollog.log(
+3 -1
View File
@@ -180,7 +180,9 @@ class Intel(linear.LinearlyMappedLayer):
position = self._initial_position
entry = self._initial_entry
if self.minimum_address > offset > self.maximum_address:
if not (
self.minimum_address <= (offset & self.address_mask) <= self.maximum_address
):
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
@@ -151,6 +151,12 @@ class ResourceAccessor(object):
raise excp
else:
raise excp
except ValueError as excp:
# Reraise errors such as proxy auth errors as offline exception errors
# Example Proxy auth error - ValueError: AbstractDigestAuthHandler does not support the following scheme: 'Negotiate'
vollog.info(f"Cannot access {url} due to {excp} - Setting OFFLINE")
constants.OFFLINE = True
raise exceptions.OfflineException(url)
except exceptions.OfflineException:
vollog.info(f"Not accessing {url} in offline mode")
raise
+1 -1
View File
@@ -152,7 +152,7 @@ class NonLinearlySegmentedLayer(
raise ValueError("SegmentedLayer must contain some segments")
if self._maxaddr is None:
mapped, _, length, _ = self._segments[-1]
self._maxaddr = mapped + length
self._maxaddr = mapped + length - 1
return self._maxaddr
@property
+21
View File
@@ -7,6 +7,27 @@ from typing import Optional, Union
from volatility3.framework import interfaces, objects, constants
def rol(value: int, count: int, max_bits: int = 64) -> int:
"""A rotate-left instruction in Python"""
max_bits_mask = (1 << max_bits) - 1
return (value << count % max_bits) & max_bits_mask | (
(value & max_bits_mask) >> (max_bits - (count % max_bits))
)
def bswap_32(value: int) -> int:
value = ((value << 8) & 0xFF00FF00) | ((value >> 8) & 0x00FF00FF)
return ((value << 16) | (value >> 16)) & 0xFFFFFFFF
def bswap_64(value: int) -> int:
low = bswap_32((value >> 32))
high = bswap_32((value & 0xFFFFFFFF))
return ((high << 32) | low) & 0xFFFFFFFFFFFFFFFF
def array_to_string(
array: "objects.Array", count: Optional[int] = None, errors: str = "replace"
) -> interfaces.objects.ObjectInterface:
+10 -7
View File
@@ -18,7 +18,7 @@ class LayerWriter(plugins.PluginInterface):
default_block_size = 0x500000
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -77,7 +77,7 @@ class LayerWriter(plugins.PluginInterface):
file_handle = open_method(preferred_name)
for i in range(0, layer.maximum_address, chunk_size):
current_chunk_size = min(chunk_size, layer.maximum_address - i)
current_chunk_size = min(chunk_size, layer.maximum_address + 1 - i)
data = layer.read(i, current_chunk_size, pad=True)
file_handle.write(data)
if progress_callback:
@@ -95,7 +95,7 @@ class LayerWriter(plugins.PluginInterface):
if not self.config["layers"]:
self.config["layers"] = []
for name in self.context.layers:
if not self.context.layers[name].metadata.get("mapped", False):
if "mapped" not in self.context.layers[name].metadata:
self.config["layers"] = [name]
for name in self.config["layers"]:
@@ -103,7 +103,8 @@ class LayerWriter(plugins.PluginInterface):
if name not in self.context.layers:
yield 0, (f"Layer Name {name} does not exist",)
else:
output_name = self.config.get("output", ".".join([name, "raw"]))
default_output_name = f"{name}.raw"
output_name = self.config.get("output", default_output_name)
try:
file_handle = self.write_layer(
self.context,
@@ -114,10 +115,12 @@ class LayerWriter(plugins.PluginInterface):
progress_callback=self._progress_callback,
)
file_handle.close()
# Update the filename, which may have changed if a file
# with the same name already existed.
output_name = file_handle.preferred_filename
except IOError as excp:
yield 0, (
f"Layer cannot be written to {self.config['output_name']}: {excp}",
)
yield 0, (f"Layer cannot be written to {output_name}: {excp}",)
yield 0, (f"Layer has been written to {output_name}",)
@@ -2,20 +2,19 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from volatility3.framework import interfaces, renderers
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class Check_creds(interfaces.plugins.PluginInterface):
"""Checks if any processes are sharing credential structures"""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
return [
@@ -46,20 +45,28 @@ class Check_creds(interfaces.plugins.PluginInterface):
tasks = pslist.PsList.list_tasks(self.context, vmlinux.name)
for task in tasks:
cred_addr = task.cred.dereference().vol.offset
task_cred_ptr = task.cred
if not (task_cred_ptr and task_cred_ptr.is_readable()):
continue
if cred_addr not in creds:
creds[cred_addr] = []
cred_addr = task_cred_ptr.dereference().vol.offset
creds.setdefault(cred_addr, [])
creds[cred_addr].append(task.pid)
for _, pids in creds.items():
for cred_addr, pids in creds.items():
if len(pids) > 1:
pid_str = ""
for pid in pids:
pid_str = pid_str + f"{pid:d}, "
pid_str = pid_str[:-2]
yield (0, [str(pid_str)])
pid_str = ", ".join([str(pid) for pid in pids])
fields = [
format_hints.Hex(cred_addr),
pid_str,
]
yield (0, fields)
def run(self):
return renderers.TreeGrid([("PIDs", str)], self._generator())
headers = [
("CredVAddr", format_hints.Hex),
("PIDs", str),
]
return renderers.TreeGrid(headers, self._generator())
@@ -0,0 +1,73 @@
# 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
#
import logging
from typing import List
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.renderers import format_hints
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
vollog = logging.getLogger(__name__)
class EBPF(plugins.PluginInterface):
"""Enumerate eBPF programs"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
]
def get_ebpf_programs(
self,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> interfaces.objects.ObjectInterface:
"""Enumerate eBPF programs walking its IDR.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate
Yields:
eBPF program objects
"""
vmlinux = context.modules[vmlinux_module_name]
if not vmlinux.has_symbol("prog_idr"):
raise exceptions.VolatilityException(
"Cannot find the eBPF prog idr. Unsupported kernel"
)
prog_idr = vmlinux.object_from_symbol("prog_idr")
for page_addr in prog_idr.get_entries():
bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True)
yield bpf_prog
def _generator(self):
for prog in self.get_ebpf_programs(self.context, self.config["kernel"]):
prog_addr = prog.vol.offset
prog_type = prog.get_type() or renderers.NotAvailableValue()
prog_tag = prog.get_tag() or renderers.NotAvailableValue()
prog_name = prog.get_name() or renderers.NotAvailableValue()
fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type)
yield (0, fields)
def run(self):
headers = [
("Address", format_hints.Hex),
("Name", str),
("Tag", str),
("Type", str),
]
return renderers.TreeGrid(headers, self._generator())
@@ -84,6 +84,7 @@ class Elfs(plugins.PluginInterface):
)
if not elf_object.is_valid():
vollog.debug("ELF object to be dumped is not valid")
return None
sections = {}
+1 -1
View File
@@ -66,7 +66,7 @@ class ABCKmsg(ABC):
self._config = config
self.vmlinux = context.modules[self._config["kernel"]]
self.layer_name = self.vmlinux.layer_name # type: ignore
self.long_unsigned_int_size = self.vmlinux.get_type("long unsigned int").size
self.long_unsigned_int_size = self.vmlinux.get_type("pointer").size
@classmethod
def run_all(
+95 -14
View File
@@ -1,27 +1,27 @@
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
import logging
import logging, datetime
from typing import List, Callable
from volatility3.framework import renderers, interfaces, constants
from volatility3.framework import renderers, interfaces, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
from volatility3.framework.symbols import linux
from volatility3.plugins.linux import pslist
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
class Lsof(plugins.PluginInterface):
"""Lists all memory maps for all processes."""
class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists open files for each processes."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
_version = (1, 2, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -45,6 +45,29 @@ class Lsof(plugins.PluginInterface):
),
]
@classmethod
def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface):
try:
dentry = filp.get_dentry()
if dentry:
inode_object = dentry.d_inode
if inode_object and inode_object.is_valid():
itype = (
inode_object.get_inode_type() or renderers.NotAvailableValue()
)
return (
inode_object.i_ino,
itype,
inode_object.i_size,
inode_object.get_file_mode(),
inode_object.get_change_time(),
inode_object.get_modification_time(),
inode_object.get_access_time(),
)
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.warning(f"Can't get inode metadata: {e}")
return None
@classmethod
def list_fds(
cls,
@@ -52,7 +75,7 @@ class Lsof(plugins.PluginInterface):
symbol_table: str,
filter_func: Callable[[int], bool] = lambda _: False,
):
linuxutils_symbol_table = None # type: ignore
linuxutils_symbol_table = None
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
if linuxutils_symbol_table is None:
if constants.BANG not in task.vol.type_name:
@@ -69,21 +92,79 @@ class Lsof(plugins.PluginInterface):
for fd_fields in fd_generator:
yield pid, task_comm, task, fd_fields
@classmethod
def list_fds_and_inodes(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
filter_func: Callable[[int], bool] = lambda _: False,
):
for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds(
context, symbol_table, filter_func
):
inode_metadata = cls.get_inode_metadata(filp)
if inode_metadata is None:
inode_metadata = tuple(
interfaces.renderers.BaseAbsentValue() for _ in range(7)
)
yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata
def _generator(self, pids, symbol_table):
filter_func = pslist.PsList.create_pid_filter(pids)
fds_generator = self.list_fds(
fds_generator = self.list_fds_and_inodes(
self.context, symbol_table, filter_func=filter_func
)
for pid, task_comm, _task, fd_fields in fds_generator:
fd_num, _filp, full_path = fd_fields
fields = (pid, task_comm, fd_num, full_path)
for (
pid,
task_comm,
task,
fd_num,
filp,
full_path,
inode_metadata,
) in fds_generator:
inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata
fields = (
pid,
task_comm,
fd_num,
full_path,
inode_num,
itype,
imode,
ctime,
mtime,
atime,
file_size,
)
yield (0, fields)
def run(self):
pids = self.config.get("pid", None)
symbol_table = self.config["kernel"]
tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)]
tree_grid_args = [
("PID", int),
("Process", str),
("FD", int),
("Path", str),
("Inode", int),
("Type", str),
("Mode", str),
("Changed", datetime.datetime),
("Modified", datetime.datetime),
("Accessed", datetime.datetime),
("Size", int),
]
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
def generate_timeline(self):
pids = self.config.get("pid", None)
symbol_table = self.config["kernel"]
for row in self._generator(pids, symbol_table):
_depth, row_data = row
description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"'
yield description, timeliner.TimeLinerType.CHANGED, row_data[7]
yield description, timeliner.TimeLinerType.MODIFIED, row_data[8]
yield description, timeliner.TimeLinerType.ACCESSED, row_data[9]
@@ -37,7 +37,7 @@ class MountInfo(plugins.PluginInterface):
_required_framework_version = (2, 2, 0)
_version = (1, 0, 0)
_version = (1, 2, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -143,10 +143,10 @@ class MountInfo(plugins.PluginInterface):
sb_opts,
)
@staticmethod
def _get_tasks_mountpoints(
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
filtered_by_pids: bool,
filtered_by_pids: bool = False,
):
seen_mountpoints = set()
for task in tasks:
@@ -184,8 +184,8 @@ class MountInfo(plugins.PluginInterface):
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
mnt_ns_ids: List[int],
mount_format: bool,
filtered_by_pids: bool,
mount_format: bool = False,
filtered_by_pids: bool = False,
) -> Iterable[Tuple[int, Tuple]]:
show_filter_warning = False
for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(
@@ -247,6 +247,37 @@ class MountInfo(plugins.PluginInterface):
"Could not filter by mount namespace id. This field is not available in this kernel."
)
@classmethod
def get_superblocks(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Yield file system superblocks based on the task's mounted filesystems.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate
Yields:
super_block: Kernel's struct super_block object
"""
# No filter so that we get all the mount namespaces from all tasks
tasks = pslist.PsList.list_tasks(context, vmlinux_module_name)
seen_sb_ptr = set()
for task, mnt, _mnt_ns_id in cls._get_tasks_mountpoints(tasks):
path_root = linux.LinuxUtilities.get_path_mnt(task, mnt)
if not path_root:
continue
sb_ptr = mnt.get_mnt_sb()
if not sb_ptr or sb_ptr in seen_sb_ptr:
continue
seen_sb_ptr.add(sb_ptr)
yield sb_ptr.dereference(), path_root
def run(self):
pids = self.config.get("pids")
mount_ns_ids = self.config.get("mntns")
@@ -0,0 +1,738 @@
# 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 typing import Iterator, List, Tuple
from volatility3 import framework
from volatility3.framework import (
constants,
interfaces,
renderers,
exceptions,
)
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols import linux
from volatility3.plugins.linux import lsmod
vollog = logging.getLogger(__name__)
@dataclass
class Proto:
name: str
hooks: Tuple[str] = field(default_factory=tuple)
PROTO_NOT_IMPLEMENTED = Proto(name="UNSPEC")
NF_INET_HOOKS = ("PRE_ROUTING", "LOCAL_IN", "FORWARD", "LOCAL_OUT", "POST_ROUTING")
NF_DEC_HOOKS = (
"PRE_ROUTING",
"LOCAL_IN",
"FORWARD",
"LOCAL_OUT",
"POST_ROUTING",
"HELLO",
"ROUTE",
)
NF_ARP_HOOKS = ("IN", "OUT", "FORWARD")
NF_NETDEV_HOOKS = ("INGRESS", "EGRESS")
LARGEST_HOOK_NUMBER = max(
len(NF_INET_HOOKS), len(NF_DEC_HOOKS), len(NF_ARP_HOOKS), len(NF_NETDEV_HOOKS)
)
class AbstractNetfilter(ABC):
"""Netfilter Abstract Base Classes handling details across various
Netfilter implementations, including constants, helpers, and common
routines.
"""
PROTO_HOOKS = (
PROTO_NOT_IMPLEMENTED, # NFPROTO_UNSPEC
Proto(name="INET", hooks=NF_INET_HOOKS), # From kernels 3.14
Proto(name="IPV4", hooks=NF_INET_HOOKS),
Proto(name="ARP", hooks=NF_ARP_HOOKS),
PROTO_NOT_IMPLEMENTED,
Proto(name="NETDEV", hooks=NF_NETDEV_HOOKS),
PROTO_NOT_IMPLEMENTED,
Proto(name="BRIDGE", hooks=NF_INET_HOOKS),
PROTO_NOT_IMPLEMENTED,
PROTO_NOT_IMPLEMENTED,
Proto(name="IPV6", hooks=NF_INET_HOOKS),
PROTO_NOT_IMPLEMENTED,
Proto(name="DECNET", hooks=NF_DEC_HOOKS), # Removed in kernel 6.1
)
NF_MAX_HOOKS = LARGEST_HOOK_NUMBER + 1
def __init__(
self, context: interfaces.context.ContextInterface, kernel_module_name: str
):
self._context = context
self.vmlinux = context.modules[kernel_module_name]
self.layer_name = self.vmlinux.layer_name
# Set data sizes
self.ptr_size = self.vmlinux.get_type("pointer").size
self.list_head_size = self.vmlinux.get_type("list_head").size
lsmod_required_version = Netfilter._required_lsmod_version
lsmod_current_version = lsmod.Lsmod._version
if not requirements.VersionRequirement.matches_required(
lsmod_required_version, lsmod_current_version
):
raise exceptions.PluginRequirementException(
f"linux.lsmod.Lsmod version not suitable: required {lsmod_required_version} found {lsmod_current_version}"
)
linuxutils_required_version = Netfilter._required_linuxutils_version
linuxutils_current_version = linux.LinuxUtilities._version
if not requirements.VersionRequirement.matches_required(
linuxutils_required_version, linuxutils_current_version
):
raise exceptions.PluginRequirementException(
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
)
modules = lsmod.Lsmod.list_modules(context, kernel_module_name)
self.handlers = linux.LinuxUtilities.generate_kernel_handler_info(
context, kernel_module_name, modules
)
@classmethod
def run_all(
cls, context: interfaces.context.ContextInterface, kernel_module_name: str
) -> Iterator[Tuple[int, str, str, int, int, str, bool]]:
"""It calls each subclass symtab_checks() to test the required
conditions to that specific kernel implementation.
Args:
context: The volatility3 context on which to operate
kernel_module_name: The name of the table containing the kernel symbols
Yields:
The kmsg records. Same as _run()
"""
vmlinux = context.modules[kernel_module_name]
implementation_inst = None # type: ignore
for subclass in framework.class_subclasses(cls):
if not subclass.symtab_checks(vmlinux=vmlinux):
vollog.log(
constants.LOGLEVEL_VVVV,
"Netfilter implementation '%s' doesn't match this memory dump",
subclass.__name__,
)
continue
vollog.log(
constants.LOGLEVEL_VVVV,
"Netfilter implementation '%s' matches!",
subclass.__name__,
)
implementation_inst = subclass(
context=context, kernel_module_name=kernel_module_name
)
# More than one class could be executed for an specific kernel version
# For instance: Netfilter Ingress hooks
yield from implementation_inst._run()
if implementation_inst is None:
vollog.error("Unsupported Netfilter kernel implementation")
def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]:
"""Iterates over namespaces and protocols, executing various callbacks that
allow customization of the code to the specific data structure used in a
particular kernel implementation
get_hooks_container(net, proto_name, hook_name)
It returns the data structure used in a specific kernel implementation
to store the hooks for a respective namespace and protocol, basically:
For Ingress hooks:
network_namespace[] -> net_device[] -> nf_hooks_ingress[]
For egress hooks:
network_namespace[] -> net_device[] -> nf_hooks_egress[]
For all the other Netfilter hooks:
<= 4.2.8
nf_hooks[]
>= 4.3
network_namespace[] -> nf.hooks[]
get_hook_ops(hook_container, proto_idx, hook_idx)
Give the 'hook_container' got in get_hooks_container(), it
returns an iterable of 'nf_hook_ops' elements for a respective protocol
and hook type.
Returns:
netns [int]: Network namespace id
proto_name [str]: Protocol name
hook_name [str]: Hook name
priority [int]: Priority
hook_ops_hook [int]: Hook address
module_name [str]: Linux kernel module name
hooked [bool]: hooked?
"""
for netns, net in self.get_net_namespaces():
for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop():
hooks_container = self.get_hooks_container(net, proto_name, hook_name)
for hook_container in hooks_container:
for hook_ops in self.get_hook_ops(
hook_container, proto_idx, hook_idx
):
if not hook_ops:
continue
priority = int(hook_ops.priority)
hook_ops_hook = hook_ops.hook
module_name = self.get_module_name_for_address(hook_ops_hook)
hooked = module_name is not None
yield netns, proto_name, hook_name, priority, hook_ops_hook, module_name, hooked
@classmethod
@abstractmethod
def symtab_checks(cls, vmlinux: interfaces.context.ModuleInterface) -> bool:
"""This method on each sublasss will be called to evaluate if the kernel
being analyzed fulfill the type & symbols requirements for the implementation.
The first class returning True will be instantiated and called via the
run() method.
Returns:
bool: True if the kernel being analyzed fulfill the class requirements.
"""
def _proto_hook_loop(self) -> Iterator[Tuple[int, str, int, str]]:
"""Flattens the protocol families and hooks"""
for proto_idx, proto in enumerate(AbstractNetfilter.PROTO_HOOKS):
if proto == PROTO_NOT_IMPLEMENTED:
continue
if proto.name not in self.subscribed_protocols():
# This protocol is not managed in this object
continue
for hook_idx, hook_name in enumerate(proto.hooks):
yield proto_idx, proto.name, hook_idx, hook_name
def build_nf_hook_ops_array(self, nf_hook_entries):
"""Function helper to build the nf_hook_ops array when it is not part of the
struct 'nf_hook_entries' definition.
nf_hook_ops was stored adjacent in memory to the nf_hook_entry array, in the
new struct 'nf_hook_entries'. However, this 'nf_hooks_ops' array 'orig_ops' is
not part of the 'nf_hook_entries' struct. So, we need to calculate the offset.
struct nf_hook_entries {
u16 num_hook_entries; /* plus padding */
struct nf_hook_entry hooks[];
//const struct nf_hook_ops *orig_ops[];
}
"""
nf_hook_entry_size = self.vmlinux.get_type("nf_hook_entry").size
orig_ops_addr = (
nf_hook_entries.hooks.vol.offset
+ nf_hook_entry_size * nf_hook_entries.num_hook_entries
)
orig_ops = self._context.object(
object_type=self.get_symbol_fullname("array"),
offset=orig_ops_addr,
subtype=self.vmlinux.get_type("pointer"),
layer_name=self.layer_name,
count=nf_hook_entries.num_hook_entries,
)
return orig_ops
def subscribed_protocols(self) -> Tuple[str]:
"""Allows to select which PROTO_HOOKS protocols will be processed by the
Netfiler subclass.
"""
# Most implementation handlers respond to these protocols, except for
# the ingress hook, which specifically handles the 'NETDEV' protocol.
# However, there is no corresponding Netfilter hook implementation for
# the INET protocol in the kernel. AFAIU, this is used as
# 'NFPROTO_INET = NFPROTO_IPV4 || NFPROTO_IPV6'
# in other parts of the kernel source code.
return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET")
def get_module_name_for_address(self, addr) -> str:
"""Helper to obtain the module and symbol name in the format needed for the
output of this plugin.
"""
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
self.vmlinux, self.handlers, addr
)
if module_name == "UNKNOWN":
module_name = None
if symbol_name != "N/A":
module_name = f"[{symbol_name}]"
return module_name
def get_net_namespaces(self):
"""Common function to retrieve the different namespaces.
From 4.3 on, all the implementations use network namespaces.
"""
nethead = self.vmlinux.object_from_symbol("net_namespace_list")
symbol_net_name = self.get_symbol_fullname("net")
for net in nethead.to_list(symbol_net_name, "list"):
net_ns_id = net.ns.inum
yield net_ns_id, net
def get_hooks_container(self, net, proto_name, hook_name):
"""Returns the data structure used in a specific kernel implementation to store
the hooks for a respective namespace and protocol.
Except for kernels < 4.3, all the implementations use network namespaces.
Also the data structure which contains the hooks, even though it changes its
implementation and/or data type, it is always in this location.
"""
yield net.nf.hooks
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
"""Given the hook_container obtained from get_hooks_container(), it
returns an iterable of 'nf_hook_ops' elements for a corresponding protocol
and hook type.
This is the most variable/unstable part of all Netfilter hook designs, it
changes almost in every single implementation.
"""
raise NotImplementedError("You must implement this method")
def get_symbol_fullname(self, symbol_basename: str) -> str:
"""Given a short symbol or type name, it returns its full name"""
return self.vmlinux.symbol_table_name + constants.BANG + symbol_basename
@staticmethod
def get_member_type(
vol_type: interfaces.objects.Template, member_name: str
) -> List[str]:
"""Returns a list of types/subtypes belonging to the given type member.
Args:
vol_type (interfaces.objects.Template): A vol3 type object
member_name (str): The member name
Returns:
list: A list of types/subtypes
"""
_size, vol_obj = vol_type.vol.members[member_name]
type_name = vol_obj.type_name
type_basename = type_name.split(constants.BANG)[1]
member_type = [type_basename]
cur_type = vol_obj
while hasattr(cur_type, "subtype"):
subtype_name = cur_type.subtype.type_name
subtype_basename = subtype_name.split(constants.BANG)[1]
member_type.append(subtype_basename)
cur_type = cur_type.subtype
return member_type
class NetfilterImp_to_4_3(AbstractNetfilter):
"""At this point, Netfilter hooks were implemented as a linked list of struct
'nf_hook_ops' type. One linked list per protocol per hook type.
It was like that until 4.2.8.
struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS];
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return vmlinux.has_symbol("nf_hooks")
def get_net_namespaces(self):
# In kernels <= 4.2.8 netfilter hooks are not implemented per namespaces
netns, net = renderers.NotAvailableValue(), renderers.NotAvailableValue()
yield netns, net
def get_hooks_container(self, net, proto_name, hook_name):
nf_hooks = self.vmlinux.object_from_symbol("nf_hooks")
if not nf_hooks:
return
yield nf_hooks
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
list_head = hook_container[proto_idx][hook_idx]
nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops")
return list_head.to_list(nf_hooks_ops_name, "list")
class NetfilterImp_4_3_to_4_9(AbstractNetfilter):
"""Netfilter hooks were added to network namepaces in 4.3.
It is still implemented as a linked list of 'struct nf_hook_ops' type but inside a
network namespace. One linked list per protocol per hook type.
struct net { ... struct netns_nf nf; ... }
struct netns_nf { ...
struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... }
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("netns_nf")
and vmlinux.get_type("netns_nf").has_member("hooks")
and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks")
== ["array", "array", "list_head"]
)
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
list_head = hook_container[proto_idx][hook_idx]
nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops")
return list_head.to_list(nf_hooks_ops_name, "list")
class NetfilterImp_4_9_to_4_14(AbstractNetfilter):
"""In this range of kernel versions, the doubly-linked lists of netfilter hooks were
replaced by an array of arrays of 'nf_hook_entry' pointers in a singly-linked lists.
struct net { ... struct netns_nf nf; ... }
struct netns_nf { ..
struct nf_hook_entry __rcu *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... }
Also in v4.10 the struct nf_hook_entry changed, a hook function pointer was added to
it. However, for simplicity of this design, we will still take the hook address from
the 'nf_hook_ops'. As per v5.0-rc2, the hook address is duplicated in both sides.
- v4.9:
struct nf_hook_entry {
struct nf_hook_entry *next;
struct nf_hook_ops ops;
const struct nf_hook_ops *orig_ops; };
- v4.10:
struct nf_hook_entry {
struct nf_hook_entry *next;
nf_hookfn *hook;
void *priv;
const struct nf_hook_ops *orig_ops; };
(*) Even though the hook address is in the struct 'nf_hook_entry', we use the
original 'nf_hook_ops' hook address value, the one which was filled by the user, to
make it uniform to all the implementations.
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
hooks_type = ["array", "array", "pointer", "nf_hook_entry"]
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("netns_nf")
and vmlinux.get_type("netns_nf").has_member("hooks")
and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type
)
def _get_hook_ops(self, hook_container, proto_idx, hook_idx):
list_head = hook_container[proto_idx][hook_idx]
nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops")
return list_head.to_list(nf_hooks_ops_name, "list")
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
nf_hook_entry_list = hook_container[proto_idx][hook_idx]
while nf_hook_entry_list:
yield nf_hook_entry_list.orig_ops
nf_hook_entry_list = nf_hook_entry_list.next
class NetfilterImp_4_14_to_4_16(AbstractNetfilter):
"""'nf_hook_ops' was removed from struct 'nf_hook_entry'. Instead, it was stored
adjacent in memory to the 'nf_hook_entry' array, in the new struct 'nf_hook_entries'
However, 'orig_ops' is not part of the 'nf_hook_entries' struct definition. So, we
have to craft it by hand.
struct net { ... struct netns_nf nf; ... }
struct netns_nf {
struct nf_hook_entries *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... }
struct nf_hook_entries {
u16 num_hook_entries; /* plus padding */
struct nf_hook_entry hooks[];
//const struct nf_hook_ops *orig_ops[]; }
struct nf_hook_entry {
nf_hookfn *hook;
void *priv; }
(*) Even though the hook address is in the struct 'nf_hook_entry', we use the
original 'nf_hook_ops' hook address value, the one which was filled by the user, to
make it uniform to all the implementations.
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
hooks_type = ["array", "array", "pointer", "nf_hook_entries"]
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("netns_nf")
and vmlinux.get_type("netns_nf").has_member("hooks")
and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type
)
def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx):
"""This allows to support different hook array implementations from this version
on. For instance, in kernels >= 4.16 this multi-dimensional array is split in
one-dimensional array of pointers to 'nf_hooks_entries' per each protocol."""
return nf_hooks_addr[proto_idx][hook_idx]
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
nf_hook_entries = self.get_nf_hook_entries(hook_container, proto_idx, hook_idx)
if not nf_hook_entries:
return
nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops")
nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries)
for nf_hook_ops_ptr in nf_hook_ops_ptr_arr:
nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name)
yield nf_hook_ops
class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16):
"""The multidimensional array of nf_hook_entries was split in a one-dimensional
array per each protocol.
struct net {
struct netns_nf nf; ... }
struct netns_nf {
struct nf_hook_entries * hooks_ipv4[NF_INET_NUMHOOKS];
struct nf_hook_entries * hooks_ipv6[NF_INET_NUMHOOKS];
struct nf_hook_entries * hooks_arp[NF_ARP_NUMHOOKS];
struct nf_hook_entries * hooks_bridge[NF_INET_NUMHOOKS];
struct nf_hook_entries * hooks_decnet[NF_DN_NUMHOOKS]; ... }
struct nf_hook_entries {
u16 num_hook_entries; /* plus padding */
struct nf_hook_entry hooks[];
//const struct nf_hook_ops *orig_ops[]; }
struct nf_hook_entry {
nf_hookfn *hook;
void *priv; }
(*) Even though the hook address is in the struct nf_hook_entry, we use the original
nf_hook_ops hook address value, the one which was filled by the user, to make it
uniform to all the implementations.
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("netns_nf")
and vmlinux.get_type("netns_nf").has_member("hooks_ipv4")
)
def get_hooks_container(self, net, proto_name, hook_name):
try:
if proto_name == "IPV4":
net_nf_hooks = net.nf.hooks_ipv4
elif proto_name == "ARP":
net_nf_hooks = net.nf.hooks_arp
elif proto_name == "BRIDGE":
net_nf_hooks = net.nf.hooks_bridge
elif proto_name == "IPV6":
net_nf_hooks = net.nf.hooks_ipv6
elif proto_name == "DECNET":
net_nf_hooks = net.nf.hooks_decnet
else:
return
yield net_nf_hooks
except AttributeError:
# Protocol family disabled at kernel compilation
# CONFIG_NETFILTER_FAMILY_ARP=n ||
# CONFIG_NETFILTER_FAMILY_BRIDGE=n ||
# CONFIG_DECNET=n
pass
def _get_nf_hook_entries_ptr(self, nf_hooks_addr, proto_idx, hook_idx):
nf_hook_entries_ptr = nf_hooks_addr[hook_idx]
return nf_hook_entries_ptr
def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx):
return nf_hooks_addr[hook_idx]
class AbstractNetfilterNetDev(AbstractNetfilter):
"""Base class to handle the Netfilter NetDev hooks.
It won't be executed. It has some common functions to all Netfilter NetDev hook
implementions.
Netfilter NetDev hooks are set per network device which belongs to a network
namespace.
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return False
def subscribed_protocols(self):
return ("NETDEV",)
def get_hooks_container(self, net, proto_name, hook_name):
net_device_type = self.vmlinux.get_type("net_device")
net_device_name = self.get_symbol_fullname("net_device")
for net_device in net.dev_base_head.to_list(net_device_name, "dev_list"):
if hook_name == "INGRESS":
if net_device_type.has_member("nf_hooks_ingress"):
# CONFIG_NETFILTER_INGRESS=y
yield net_device.nf_hooks_ingress
elif hook_name == "EGRESS":
if net_device_type.has_member("nf_hooks_egress"):
# CONFIG_NETFILTER_EGRESS=y
yield net_device.nf_hooks_egress
class NetfilterNetDevImp_4_2_to_4_9(AbstractNetfilterNetDev):
"""This is the first version of Netfilter Ingress hooks which was implemented using
a doubly-linked list of 'nf_hook_ops'.
struct list_head nf_hooks_ingress;
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
hooks_type = ["list_head"]
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("net_device")
and vmlinux.get_type("net_device").has_member("nf_hooks_ingress")
and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress")
== hooks_type
)
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
nf_hooks_ingress = hook_container
nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops")
return nf_hooks_ingress.to_list(nf_hook_ops_name, "list")
class NetfilterNetDevImp_4_9_to_4_14(AbstractNetfilterNetDev):
"""In 4.9 it was changed to a simple singly-linked list.
struct nf_hook_entry * nf_hooks_ingress;
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
hooks_type = ["pointer", "nf_hook_entry"]
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("net_device")
and vmlinux.get_type("net_device").has_member("nf_hooks_ingress")
and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress")
== hooks_type
)
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
nf_hooks_ingress_ptr = hook_container
if not nf_hooks_ingress_ptr:
return
while nf_hooks_ingress_ptr:
nf_hook_entry = nf_hooks_ingress_ptr.dereference()
orig_ops = nf_hook_entry.orig_ops.dereference()
yield orig_ops
nf_hooks_ingress_ptr = nf_hooks_ingress_ptr.next
class NetfilterNetDevImp_4_14_to_latest(AbstractNetfilterNetDev):
"""In 4.14 the hook list was converted to an array of pointers inside the struct
'nf_hook_entries':
struct nf_hook_entries * nf_hooks_ingress;
struct nf_hook_entries {
u16 num_hook_entries;
struct nf_hook_entry hooks[];
//const struct nf_hook_ops *orig_ops[]; }
"""
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
hooks_type = ["pointer", "nf_hook_entries"]
return (
vmlinux.has_symbol("net_namespace_list")
and vmlinux.has_type("net_device")
and vmlinux.get_type("net_device").has_member("nf_hooks_ingress")
and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress")
== hooks_type
)
def get_hook_ops(self, hook_container, proto_idx, hook_idx):
nf_hook_entries = hook_container
if not nf_hook_entries:
return
nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops")
nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries)
for nf_hook_ops_ptr in nf_hook_ops_ptr_arr:
nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name)
yield nf_hook_ops
class Netfilter(interfaces.plugins.PluginInterface):
"""Lists Netfilter hooks."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_required_linuxutils_version = (2, 1, 0)
_required_lsmod_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=cls._required_lsmod_version
),
requirements.VersionRequirement(
name="linuxutils",
component=linux.LinuxUtilities,
version=cls._required_linuxutils_version,
),
]
def _format_fields(self, fields):
(
netns,
proto_name,
hook_name,
priority,
hook_func,
module_name,
hooked,
) = fields
return (
netns,
proto_name,
hook_name,
priority,
format_hints.Hex(hook_func),
module_name,
str(hooked),
)
def _generator(self):
kernel_module_name = self.config["kernel"]
for fields in AbstractNetfilter.run_all(
context=self.context, kernel_module_name=kernel_module_name
):
yield (0, self._format_fields(fields))
def run(self):
headers = [
("Net NS", int),
("Proto", str),
("Hook", str),
("Priority", int),
("Handler", format_hints.Hex),
("Module", str),
("Is Hooked", str),
]
return renderers.TreeGrid(headers, self._generator())
@@ -0,0 +1,531 @@
# 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
#
import math
import logging
import datetime
from dataclasses import dataclass, astuple
from typing import List, Set, Type, Iterable
from volatility3.framework import renderers, interfaces
from volatility3.framework.renderers import format_hints
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
from volatility3.plugins import timeliner
from volatility3.plugins.linux import mountinfo
vollog = logging.getLogger(__name__)
@dataclass
class InodeUser:
"""Inode user representation, featuring augmented information and formatted fields.
This is the data the plugin will eventually display.
"""
superblock_addr: int
mountpoint: str
device: str
inode_num: int
inode_addr: int
type: str
inode_pages: int
cached_pages: int
file_mode: str
access_time: str
modification_time: str
change_time: str
path: str
@dataclass
class InodeInternal:
"""Inode internal representation containing only the core objects
Fields:
superblock: 'super_block' struct
mountpoint: Superblock mountpoint path
inode: 'inode' struct
path: Dentry full path
"""
superblock: interfaces.objects.ObjectInterface
mountpoint: str
inode: interfaces.objects.ObjectInterface
path: str
def to_user(
self, kernel_layer: interfaces.layers.TranslationLayerInterface
) -> InodeUser:
"""Augment the inode information to be presented to the user
Args:
kernel_layer: The kernel layer to obtain the page size
Returns:
An InodeUser dataclass
"""
# Ensure all types are atomic immutable. Otherwise, astuple() will take a long
# time doing a deepcopy of the Volatility objects.
superblock_addr = self.superblock.vol.offset
device = f"{self.superblock.major}:{self.superblock.minor}"
inode_num = int(self.inode.i_ino)
inode_addr = self.inode.vol.offset
inode_type = self.inode.get_inode_type() or renderers.UnparsableValue()
# Round up the number of pages to fit the inode's size
inode_pages = int(math.ceil(self.inode.i_size / float(kernel_layer.page_size)))
cached_pages = int(self.inode.i_mapping.nrpages)
file_mode = self.inode.get_file_mode()
access_time_dt = self.inode.get_access_time()
modification_time_str = self.inode.get_modification_time()
change_time_str = self.inode.get_change_time()
inode_user = InodeUser(
superblock_addr=superblock_addr,
mountpoint=self.mountpoint,
device=device,
inode_num=inode_num,
inode_addr=inode_addr,
type=inode_type,
inode_pages=inode_pages,
cached_pages=cached_pages,
file_mode=file_mode,
access_time=access_time_dt,
modification_time=modification_time_str,
change_time=change_time_str,
path=self.path,
)
return inode_user
class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists files from memory"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0)
),
requirements.ListRequirement(
name="type",
description="List of space-separated file type filters i.e. --type REG DIR",
element_type=str,
optional=True,
),
requirements.StringRequirement(
name="find",
description="Filename (full path) to find",
optional=True,
),
]
@staticmethod
def _follow_symlink(
inode: interfaces.objects.ObjectInterface,
symlink_path: str,
) -> str:
"""Follows (fast) symlinks (kernels >= 4.2.x).
Fast symlinks are filesystem agnostic.
Args:
inode: The inode (or pointer) to dump
symlink_path: The symlink name
Returns:
If it can resolve the symlink, it returns a string "symlink_path -> target_path"
Otherwise, it returns the same symlink_path
"""
# i_link (fast symlinks) were introduced in 4.2
if inode and inode.is_link and inode.has_member("i_link") and inode.i_link:
i_link_str = inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
symlink_path = f"{symlink_path} -> {i_link_str}"
return symlink_path
@classmethod
def _walk_dentry(
cls,
seen_dentries: Set[int],
root_dentry: interfaces.objects.ObjectInterface,
parent_dir: str,
):
"""Walks dentries recursively
Args:
seen_dentries: A set to ensure each dentry is processed only once
root_dentry: Root dentry object
parent_dir: Parent directory path
Yields:
file_path: Filename including path
dentry: Dentry object
"""
for dentry in root_dentry.get_subdirs():
dentry_addr = dentry.vol.offset
# corruption
if dentry_addr == root_dentry.vol.offset:
continue
if dentry_addr in seen_dentries:
continue
seen_dentries.add(dentry_addr)
inode = dentry.d_inode
if not (inode and inode.is_valid()):
continue
# This allows us to have consistent paths
if dentry.d_name.name:
basename = dentry.d_name.name_as_str()
# Do NOT use os.path.join() below
file_path = parent_dir + "/" + basename
else:
continue
yield file_path, dentry
if inode.is_dir:
yield from cls._walk_dentry(seen_dentries, dentry, parent_dir=file_path)
@classmethod
def get_inodes(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Iterable[InodeInternal]:
"""Retrieves the inodes from the superblocks
Args:
context: The context that the plugin will operate within
vmlinux_module_name: The name of the kernel module on which to operate
Yields:
An InodeInternal object
"""
superblocks_iter = mountinfo.MountInfo.get_superblocks(
context=context,
vmlinux_module_name=vmlinux_module_name,
)
seen_inodes = set()
seen_dentries = set()
for superblock, mountpoint in superblocks_iter:
parent_dir = "" if mountpoint == "/" else mountpoint
# Superblock root dentry
root_dentry_ptr = superblock.s_root
if not root_dentry_ptr:
continue
root_dentry = root_dentry_ptr.dereference()
# Dentry sanity check
if not root_dentry.is_root():
continue
# More dentry/inode sanity checks
root_inode_ptr = root_dentry.d_inode
if not root_inode_ptr:
continue
root_inode = root_inode_ptr.dereference()
if not root_inode.is_valid():
continue
# Inode already processed?
if root_inode_ptr in seen_inodes:
continue
seen_inodes.add(root_inode_ptr)
root_path = mountpoint
inode_in = InodeInternal(
superblock=superblock,
mountpoint=mountpoint,
inode=root_inode,
path=root_path,
)
yield inode_in
# Children
for file_path, file_dentry in cls._walk_dentry(
seen_dentries, root_dentry, parent_dir
):
if not file_dentry:
continue
# Dentry/inode sanity checks
file_inode_ptr = file_dentry.d_inode
if not file_inode_ptr:
continue
file_inode = file_inode_ptr.dereference()
if not file_inode.is_valid():
continue
# Inode already processed?
if file_inode_ptr in seen_inodes:
continue
seen_inodes.add(file_inode_ptr)
file_path = cls._follow_symlink(file_inode_ptr, file_path)
inode_in = InodeInternal(
superblock=superblock,
mountpoint=mountpoint,
inode=file_inode,
path=file_path,
)
yield inode_in
def _generator(self):
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
inodes_iter = self.get_inodes(
context=self.context,
vmlinux_module_name=vmlinux_module_name,
)
types_filter = self.config["type"]
for inode_in in inodes_iter:
if types_filter and inode_in.inode.get_inode_type() not in types_filter:
continue
if self.config["find"]:
if inode_in.path == self.config["find"]:
inode_out = inode_in.to_user(vmlinux_layer)
yield (0, astuple(inode_out))
break # Only the first match
else:
inode_out = inode_in.to_user(vmlinux_layer)
yield (0, astuple(inode_out))
def generate_timeline(self):
"""Generates tuples of (description, timestamp_type, timestamp)
These need not be generated in any particular order, sorting
will be done later
"""
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
inodes_iter = self.get_inodes(
context=self.context,
vmlinux_module_name=vmlinux_module_name,
)
for inode_in in inodes_iter:
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.CHANGE, inode_out.change_time
@staticmethod
def format_fields_with_headers(headers, generator):
"""Uses the headers type to cast the fields obtained from the generator"""
for level, fields in generator:
formatted_fields = []
for header, field in zip(headers, fields):
header_type = header[1]
if isinstance(
field, (header_type, interfaces.renderers.BaseAbsentValue)
):
formatted_field = field
else:
formatted_field = header_type(field)
formatted_fields.append(formatted_field)
yield level, formatted_fields
def run(self):
headers = [
("SuperblockAddr", format_hints.Hex),
("MountPoint", str),
("Device", str),
("InodeNum", int),
("InodeAddr", format_hints.Hex),
("FileType", str),
("InodePages", int),
("CachedPages", int),
("FileMode", str),
("AccessTime", datetime.datetime),
("ModificationTime", datetime.datetime),
("ChangeTime", datetime.datetime),
("FilePath", str),
]
return renderers.TreeGrid(
headers, self.format_fields_with_headers(headers, self._generator())
)
class InodePages(plugins.PluginInterface):
"""Lists and recovers cached inode pages"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="files", plugin=Files, version=(1, 0, 0)
),
requirements.StringRequirement(
name="find",
description="Filename (full path) to find ",
optional=True,
),
requirements.IntRequirement(
name="inode",
description="Inode address",
optional=True,
),
requirements.StringRequirement(
name="dump",
description="Output file path",
optional=True,
),
]
@staticmethod
def write_inode_content_to_file(
inode: interfaces.objects.ObjectInterface,
filename: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
) -> None:
"""Extracts the inode's contents from the page cache and saves them to a file
Args:
inode: The inode to dump
filename: Filename for writing the inode content
open_method: class for constructing output files
vmlinux_layer: The kernel layer to obtain the page size
"""
if not inode.is_reg:
vollog.error("The inode is not a regular file")
return
# By using truncate/seek, provided the filesystem supports it, a sparse file will be
# created, saving both disk space and I/O time.
# Additionally, using the page index will guarantee that each page is written at the
# appropriate file position.
try:
with open_method(filename) as f:
inode_size = inode.i_size
f.truncate(inode_size)
for page_idx, page_content in inode.get_contents():
current_fp = page_idx * vmlinux_layer.page_size
max_length = inode_size - current_fp
page_bytes = page_content[:max_length]
if current_fp + len(page_bytes) > inode_size:
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.object,
inode_size,
page_idx,
)
f.seek(current_fp)
f.write(page_bytes)
except IOError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
def _generator(self):
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
if self.config["inode"] and self.config["find"]:
vollog.error("Cannot use --inode and --find simultaneously")
return
if self.config["find"]:
inodes_iter = Files.get_inodes(
context=self.context,
vmlinux_module_name=vmlinux_module_name,
)
for inode_in in inodes_iter:
if inode_in.path == self.config["find"]:
inode = inode_in.inode
break # Only the first match
elif self.config["inode"]:
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
else:
vollog.error("You must use either --inode or --find")
return
if not inode.is_reg:
vollog.error("The inode is not a regular file")
return
inode_size = inode.i_size
if not inode.is_valid():
vollog.error("Invalid inode at 0x%x", self.config["inode"])
return
for page_obj in inode.get_pages():
page_vaddr = page_obj.vol.offset
page_paddr = page_obj.to_paddr()
page_mapping_addr = page_obj.mapping
page_index = int(page_obj.index)
page_file_offset = page_index * vmlinux_layer.page_size
dump_safe = page_file_offset < inode_size
page_flags_list = page_obj.get_flags_list()
page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list])
fields = (
page_vaddr,
page_paddr,
page_mapping_addr,
page_index,
dump_safe,
page_flags,
)
yield 0, fields
if self.config["dump"]:
filename = self.config["dump"]
vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename)
self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer)
def run(self):
headers = [
("PageVAddr", format_hints.Hex),
("PagePAddr", format_hints.Hex),
("MappingAddr", format_hints.Hex),
("Index", int),
("DumpSafe", bool),
("Flags", str),
]
return renderers.TreeGrid(
headers, Files.format_fields_with_headers(headers, self._generator())
)
@@ -0,0 +1,255 @@
# 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
#
import logging
from typing import List
from volatility3.framework import renderers, interfaces, constants
from volatility3.framework.symbols import linux
from volatility3.framework.renderers import format_hints
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class PIDHashTable(plugins.PluginInterface):
"""Enumerates processes through the PID hash table"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
),
requirements.BooleanRequirement(
name="decorate_comm",
description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets",
optional=True,
default=False,
),
]
def _is_valid_task(self, task) -> bool:
return bool(task and task.pid > 0 and task.parent.is_readable())
def _get_pidtype_pid(self):
vmlinux = self.context.modules[self.config["kernel"]]
# The pid_type enumeration is present since 2.5.37, just in case
pid_type_enum = vmlinux.get_enumeration("pid_type")
if not pid_type_enum:
vollog.error("Cannot find pid_type enum. Unsupported kernel")
return None
pidtype_pid = pid_type_enum.choices.get("PIDTYPE_PID")
if pidtype_pid is None:
vollog.error("Cannot find PIDTYPE_PID. Unsupported kernel")
return None
# Typically PIDTYPE_PID = 0
return pidtype_pid
def _get_pidhash_array(self):
vmlinux = self.context.modules[self.config["kernel"]]
pidhash_shift = vmlinux.object_from_symbol("pidhash_shift")
pidhash_size = 1 << pidhash_shift
array_type_name = vmlinux.symbol_table_name + constants.BANG + "array"
pidhash_ptr = vmlinux.object_from_symbol("pid_hash")
# pidhash is an array of hlist_heads
pidhash = self._context.object(
array_type_name,
offset=pidhash_ptr,
subtype=vmlinux.get_type("hlist_head"),
count=pidhash_size,
layer_name=vmlinux.layer_name,
)
return pidhash
def _walk_upid(self, seen_upids, upid):
vmlinux = self.context.modules[self.config["kernel"]]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
while upid and vmlinux_layer.is_valid(upid.vol.offset):
if upid.vol.offset in seen_upids:
break
seen_upids.add(upid.vol.offset)
pid_chain = upid.pid_chain
if not (pid_chain.next and pid_chain.next.is_readable()):
break
upid = linux.LinuxUtilities.container_of(
pid_chain.next, "upid", "pid_chain", vmlinux
)
def _get_upids(self):
vmlinux = self.context.modules[self.config["kernel"]]
# 2.6.24 <= kernels < 4.15
pidhash = self._get_pidhash_array()
seen_upids = set()
for hlist in pidhash:
# each entry in the hlist is a upid which is wrapped in a pid
ent = hlist.first
while ent and ent.is_readable():
# upid->pid_chain exists 2.6.24 <= kernel < 4.15
upid = linux.LinuxUtilities.container_of(
ent.vol.offset, "upid", "pid_chain", vmlinux
)
if upid.vol.offset in seen_upids:
break
self._walk_upid(seen_upids, upid)
ent = ent.next
return seen_upids
def _pid_hash_implementation(self):
vmlinux = self.context.modules[self.config["kernel"]]
# 2.6.24 <= kernels < 4.15
task_pids_off = vmlinux.get_type("task_struct").relative_child_offset("pids")
pidtype_pid = self._get_pidtype_pid()
for upid in self._get_upids():
pid = linux.LinuxUtilities.container_of(upid, "pid", "numbers", vmlinux)
if not pid:
continue
pid_tasks_0 = pid.tasks[pidtype_pid].first
if not (pid_tasks_0 and pid_tasks_0.is_readable()):
continue
task = vmlinux.object(
"task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True
)
if self._is_valid_task(task):
yield task
def _task_for_radix_pid_node(self, nodep):
vmlinux = self.context.modules[self.config["kernel"]]
# kernels >= 4.15
pid = vmlinux.object("pid", offset=nodep, absolute=True)
pidtype_pid = self._get_pidtype_pid()
pid_tasks_0 = pid.tasks[pidtype_pid].first
if not (pid_tasks_0 and pid_tasks_0.is_readable()):
return None
task_struct_type = vmlinux.get_type("task_struct")
if task_struct_type.has_member("pids"):
member = "pids"
elif task_struct_type.has_member("pid_links"):
member = "pid_links"
else:
return None
task_pids_off = task_struct_type.relative_child_offset(member)
task = vmlinux.object(
"task_struct", offset=pid_tasks_0 - task_pids_off, absolute=True
)
return task
def _pid_namespace_idr(self):
vmlinux = self.context.modules[self.config["kernel"]]
# kernels >= 4.15
ns_addr = vmlinux.get_symbol("init_pid_ns").address
ns = vmlinux.object("pid_namespace", offset=ns_addr)
for page_addr in ns.idr.get_entries():
task = self._task_for_radix_pid_node(page_addr)
if self._is_valid_task(task):
yield task
def _determine_pid_func(self):
vmlinux = self.context.modules[self.config["kernel"]]
pid_hash = vmlinux.has_symbol("pid_hash") and vmlinux.has_symbol(
"pidhash_shift"
) # 2.5.55 <= kernels < 4.15
has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type(
"pid"
).has_member(
"numbers"
) # kernels >= 2.6.24
has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type(
"upid"
).has_member(
"pid_chain"
) # 2.6.24 <= kernels < 4.15
# kernels >= 4.15
pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type(
"pid_namespace"
).has_member("idr")
if pid_idr:
# kernels >= 4.15
return self._pid_namespace_idr
elif pid_hash and has_pid_numbers and has_pid_numbers and has_pid_chain:
# 2.6.24 <= kernels < 4.15
return self._pid_hash_implementation
return None
def get_tasks(self) -> interfaces.objects.ObjectInterface:
"""Enumerates processes through the PID hash table
Yields:
task_struct objects
"""
pid_func = self._determine_pid_func()
if not pid_func:
vollog.error("Cannot determine which PID hash table this kernel is using")
return
yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid))
def _generator(
self, decorate_comm: bool = False
) -> interfaces.objects.ObjectInterface:
for task in self.get_tasks():
offset, pid, tid, ppid, name = pslist.PsList.get_task_fields(
task, decorate_comm
)
fields = format_hints.Hex(offset), pid, tid, ppid, name
yield 0, fields
def run(self):
decorate_comm = self.config.get("decorate_comm")
headers = [
("OFFSET", format_hints.Hex),
("PID", int),
("TID", int),
("PPID", int),
("COMM", str),
]
return renderers.TreeGrid(headers, self._generator(decorate_comm=decorate_comm))
@@ -142,6 +142,8 @@ class PsList(interfaces.plugins.PluginInterface):
file_output = str(file_handle.preferred_filename)
file_handle.close()
break
else:
file_output = "VMA start matching task start_code not found"
return file_output
def _generator(
+12 -11
View File
@@ -22,7 +22,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (2, 0, 0)
def __init__(self, vmlinux, task):
self._vmlinux = vmlinux
@@ -151,17 +151,15 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
bpfprog = sock_filter.prog
BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter
try:
bpfprog_type = bpfprog.get_type()
if bpfprog_type == BPF_PROG_TYPE_UNSPEC:
return # cBPF filter
except AttributeError:
bpfprog_type = bpfprog.get_type()
if not bpfprog_type:
# kernel < 3.18.140, it's a cBPF filter
return None
BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter
if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER:
if bpfprog_type == "BPF_PROG_TYPE_UNSPEC":
return None # cBPF filter
if bpfprog_type != "BPF_PROG_TYPE_SOCKET_FILTER":
socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})"
vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket")
return None
@@ -509,7 +507,7 @@ class Sockstat(plugins.PluginInterface):
dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset
fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func)
for _pid, _task_comm, task, fd_fields in fd_generator:
for _pid, task_comm, task, fd_fields in fd_generator:
fd_num, filp, _full_path = fd_fields
if filp.f_op not in (sfop_addr, dfop_addr):
@@ -550,7 +548,7 @@ class Sockstat(plugins.PluginInterface):
except AttributeError:
netns_id = NotAvailableValue()
yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields
yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields
def _format_fields(self, sock_stat, protocol):
"""Prepare the socket fields to be rendered
@@ -597,6 +595,7 @@ class Sockstat(plugins.PluginInterface):
)
for (
task_comm,
task,
netns_id,
fd_num,
@@ -619,6 +618,7 @@ class Sockstat(plugins.PluginInterface):
fields = (
netns_id,
task_comm,
task.pid,
fd_num,
format_hints.Hex(sock.vol.offset),
@@ -638,6 +638,7 @@ class Sockstat(plugins.PluginInterface):
tree_grid_args = [
("NetNS", int),
("Process Name", str),
("Pid", int),
("FD", int),
("Sock Offset", format_hints.Hex),
@@ -31,10 +31,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0)
),
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0)
),
requirements.ModuleRequirement(
name="kernel",
@@ -69,19 +66,29 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
# get the proc_layer object from the context
proc_layer = self.context.layers[proc_layer_name]
# scan the process layer with the yarascanner
for offset, rule_name, name, value in proc_layer.scan(
context=self.context,
scanner=yarascan.YaraScanner(rules=rules),
sections=self.get_vma_maps(task),
):
yield 0, (
format_hints.Hex(offset),
task.tgid,
rule_name,
name,
value,
)
for start, end in self.get_vma_maps(task):
for match in rules.match(
data=proc_layer.read(start, end - start, True)
):
if yarascan.YaraScan.yara_returns_instances():
for match_string in match.strings:
for instance in match_string.instances:
yield 0, (
format_hints.Hex(instance.offset + start),
task.UniqueProcessId,
match.rule,
match_string.identifier,
instance.matched_data,
)
else:
for offset, name, value in match.strings:
yield 0, (
format_hints.Hex(offset + start),
task.tgid,
match.rule,
name,
value,
)
@staticmethod
def get_vma_maps(
+25 -16
View File
@@ -45,6 +45,7 @@ class Timeliner(interfaces.plugins.PluginInterface):
orders the results by time."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -105,7 +106,9 @@ class Timeliner(interfaces.plugins.PluginInterface):
data = item[1]
def sortable(timestamp):
max_date = datetime.datetime(day=1, month=12, year=datetime.MAXYEAR)
max_date = datetime.datetime(
day=1, month=12, year=datetime.MAXYEAR, tzinfo=datetime.timezone.utc
)
if isinstance(timestamp, interfaces.renderers.BaseAbsentValue):
return max_date
return timestamp
@@ -183,22 +186,23 @@ class Timeliner(interfaces.plugins.PluginInterface):
plugin_name,
self._sanitize_body_format(item),
self._text_format(
times.get(TimeLinerType.ACCESSED, "")
times.get(TimeLinerType.ACCESSED, "0")
),
self._text_format(
times.get(TimeLinerType.MODIFIED, "")
times.get(TimeLinerType.MODIFIED, "0")
),
self._text_format(
times.get(TimeLinerType.CHANGED, "")
times.get(TimeLinerType.CHANGED, "0")
),
self._text_format(
times.get(TimeLinerType.CREATED, "")
times.get(TimeLinerType.CREATED, "0")
),
)
)
except Exception:
except Exception as e:
vollog.log(
logging.INFO, f"Exception occurred running plugin: {plugin_name}"
logging.INFO,
f"Exception occurred running plugin: {plugin_name}: {e}",
)
vollog.log(logging.DEBUG, traceback.format_exc())
@@ -243,6 +247,18 @@ class Timeliner(interfaces.plugins.PluginInterface):
filter_list = self.config["plugin-filter"]
# Identify plugins that we can run which output datetimes
for plugin_class in self.usable_plugins:
if not issubclass(plugin_class, TimeLinerInterface):
# get_usable_plugins() should filter this, but adding a safeguard just in case
continue
if filter_list and not any(
[
filter in plugin_class.__module__ + "." + plugin_class.__name__
for filter in filter_list
]
):
continue
try:
automagics = automagic.choose_automagic(self.automagics, plugin_class)
@@ -274,15 +290,8 @@ class Timeliner(interfaces.plugins.PluginInterface):
config_value,
)
if isinstance(plugin, TimeLinerInterface):
if not len(filter_list) or any(
[
filter
in plugin.__module__ + "." + plugin.__class__.__name__
for filter in filter_list
]
):
plugins_to_run.append(plugin)
plugins_to_run.append(plugin)
except exceptions.UnsatisfiedException as excp:
# Remove the failed plugin from the list and continue
vollog.debug(
+217
View File
@@ -0,0 +1,217 @@
# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import enum
import logging
import os
import struct
from typing import Dict, List
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import configuration, plugins
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
vollog = logging.getLogger(__name__)
class VMCSTest(enum.IntFlag):
VMCS_ABORT_INVALID = enum.auto()
VMCS_LINK_PTR_IS_NOT_FS = enum.auto()
VMCS_HOST_CR4_NO_VTX = enum.auto()
VMCS_CR3_IS_ZERO = enum.auto()
VMCS_GUEST_CR4_RESERVED = enum.auto()
class PageStartScanner(interfaces.layers.ScannerInterface):
def __init__(self, signatures: List[bytes], page_size: int = 0x1000):
super().__init__()
if not len(signatures):
raise ValueError("No signatures passed to constructor")
self._siglen = len(signatures[0])
for item in signatures:
if len(item) != self._siglen:
raise ValueError(
"Signatures of different lengths passed to PageStartScanner"
)
self._signatures = signatures
self._page_size = page_size
def __call__(self, data: bytes, data_offset: int):
"""Scans only the start of every page, to see whether a signature is present or not"""
for page_start in range(
data_offset % self._page_size, len(data), self._page_size
):
if data[page_start : page_start + self._siglen] in self._signatures:
yield (
page_start + data_offset,
data[page_start : page_start + self._siglen],
)
class Vmscan(plugins.PluginInterface):
"""Scans for Intel VT-d structues and generates VM volatility configs for them"""
_required_framework_version = (2, 2, 0)
_version = (1, 0, 0)
STRICTLY_REQUIRED_TESTS = {
VMCSTest.VMCS_ABORT_INVALID,
VMCSTest.VMCS_LINK_PTR_IS_NOT_FS,
VMCSTest.VMCS_HOST_CR4_NO_VTX,
}
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.TranslationLayerRequirement(
name="primary", description="Physical base memory layer"
),
requirements.IntRequirement(
name="log-threshold",
description="Number of criteria failed to log to debug output",
default=2,
optional=True,
),
]
# Scan for VMCS structures based on the known VMCS structures
# found in symbols/vmcs directory
def _gather_vmcs_structures(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[bytes, str]:
"""Enumerate all JSON files containing VMCS information and return the structures
Signatures can be generated using data extracted using the vmcs_layout tool at
https://github.com/google/rekall/tree/master/tools/linux/vmcs_layout
Args:
context: The volatility context to work against
config_path: The location to store symbol table configurations under
Returns:
A dictionary of pattern bytes to the string representation of the architecture
"""
filenames = intermed.IntermediateSymbolTable.file_symbol_url(
os.path.join("generic", "vmcs")
)
table_names = []
for filename in filenames:
base_name = os.path.basename(filename).split(".")[0]
table_name = intermed.IntermediateSymbolTable.create(
context,
configuration.path_join(config_path, "vmcs"),
os.path.join("generic", "vmcs"),
filename=base_name,
)
table_names.append(table_name)
result = {}
for table_name in table_names:
symbol_table = context.symbol_space[table_name]
revision_id = struct.pack(
"<I", int(symbol_table.get_symbol("revision_id").constant_data)
)
result[revision_id] = table_name
return result
@classmethod
def _verify_vmcs_page(
cls,
context: interfaces.context.ContextInterface,
vmcs: interfaces.objects.ObjectInterface,
) -> List[str]:
"""Runs tests to verify whether a block of data is a VMCS page
Some tests based on the Hypervisor Memory Forensics paper by
Mariano Graziano, Andrea Lanzi and Davide Balzarotti
Args:
context: The volatility context to be used for this call
vmcs: The instantiated VMCS object to verify
Returns:
The list of failed criteria that the VMCS did not meet
"""
# The VMCS should have been constructed on the physical layer (even a nested VMCS)
physical_layer_name = vmcs.vol.layer_name
failed_tests: VMCSTest = VMCSTest(0)
# The abort field must be valid (generally 0, although other abort codes may exist)
if context.layers[physical_layer_name].read(vmcs.vol.offset + 4, 4) not in [
b"\x00\x00\x00\x00"
]:
failed_tests |= VMCSTest.VMCS_ABORT_INVALID
# The vmcs link pointer is supposed to always be set
if vmcs.vmcs_link_ptr != 0xFFFFFFFFFFFFFFFF:
failed_tests |= VMCSTest.VMCS_LINK_PTR_IS_NOT_FS
# To have a VMCS the host needs the VTx bit set in CR4, this can false positive often when all bits are set
if (vmcs.host_cr4 & 1 << 13) == 0:
failed_tests |= VMCSTest.VMCS_HOST_CR4_NO_VTX
# The guest CR3 is *exceptionally* unlikely to be 0 and the guest cr4 is likely to have some bits unset
if (vmcs.guest_cr3 == 0) or (vmcs.host_cr3 == 0):
failed_tests |= VMCSTest.VMCS_CR3_IS_ZERO
# CR4 registers have certain bits reserved that should not be set
if vmcs.guest_cr4 & 0xFFFFFFFFFF889000:
failed_tests |= VMCSTest.VMCS_GUEST_CR4_RESERVED
if failed_tests and failed_tests.name:
failed_list = failed_tests.name.split("|")
return failed_list
return []
def _generator(self):
# Gather VMCS structures
structures = self._gather_vmcs_structures(self.context, self.config_path)
# Scan memory for them
layer = self.context.layers[self.config["primary"]]
# Try to move down to the highest physical layer
if layer.config.get("memory_layer"):
layer = self.context.layers[layer.config["memory_layer"]]
# Run the scan
for offset, match in layer.scan(
self.context,
PageStartScanner(list(structures.keys())),
self._progress_callback,
):
try:
vmcs = self.context.object(
structures[match] + constants.BANG + "_VMCS",
layer.name,
offset=offset,
)
failed_list = self._verify_vmcs_page(self.context, vmcs)
if not failed_list:
yield (
0,
(
structures[match],
format_hints.Hex(vmcs.vol.offset),
format_hints.Hex(vmcs.ept),
format_hints.Hex(vmcs.guest_cr3),
),
)
if len(failed_list) <= self.config["log-threshold"]:
vollog.debug(
f"Potential {structures[match]} VMCS found at {vmcs.vol.offset:x} with failed criteria: {failed_list}"
)
except (exceptions.InvalidAddressException, AttributeError):
# Not what we're looking for
continue
def run(self):
return renderers.TreeGrid(
[
("Architecture", str),
("VMCS Physical offset", format_hints.Hex),
("EPT", format_hints.Hex),
("Guest CR3", format_hints.Hex),
],
self._generator(),
)
@@ -2,16 +2,24 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
import contextlib
from typing import List, Iterable, Tuple, Optional, Union
import logging
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
from volatility3.framework import constants, exceptions, renderers, interfaces, symbols
from volatility3.framework import (
constants,
exceptions,
interfaces,
objects,
renderers,
symbols,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import versions
from volatility3.plugins.windows import ssdt
from volatility3.framework.symbols.windows.extensions import callbacks
from volatility3.plugins.windows import driverirp, handles, poolscanner, ssdt
vollog = logging.getLogger(__name__)
@@ -20,7 +28,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
"""Lists kernel callbacks and notification routines."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -33,27 +41,154 @@ class Callbacks(interfaces.plugins.PluginInterface):
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="driverirp", plugin=driverirp.DriverIrp, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="handles", plugin=handles.Handles, version=(1, 0, 0)
),
]
@staticmethod
def create_callback_table(
@classmethod
def create_callback_scan_constraints(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
config_path: str,
) -> str:
"""Creates a symbol table for a set of callbacks.
is_vista_or_above: bool,
) -> List[poolscanner.PoolConstraint]:
"""Creates a list of Pool Tag Constraints for callback objects.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
symbol_table: The name of an existing symbol table containing the kernel symbols
config_path: The configuration path within the context of the symbol table to create
symbol_table: The name of an existing symbol table containing the symbols / types
is_vista_or_above: A boolean indicating whether the OS version is Vista or newer.
Returns:
The name of the constructed callback table
The list containing the built constraints.
"""
native_types = context.symbol_space[symbol_table].natives
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
table_mapping = {"nt_symbols": symbol_table}
constraints = cls._create_default_scan_constraints(context, symbol_table)
if is_vista_or_above:
constraints += cls._create_scan_constraints_vista_and_above(symbol_table)
return constraints
@staticmethod
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
generic_callback_size = context.symbol_space.get_type(
symbol_table + constants.BANG + "_GENERIC_CALLBACK"
).size
notification_packet_size = context.symbol_space.get_type(
symbol_table + constants.BANG + "_NOTIFICATION_PACKET"
).size
return [
poolscanner.PoolConstraint(
b"IoFs",
type_name=symbol_table + constants.BANG + "_NOTIFICATION_PACKET",
size=(notification_packet_size, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
),
poolscanner.PoolConstraint(
b"IoSh",
type_name=symbol_table + constants.BANG + "_SHUTDOWN_PACKET",
size=(shutdown_packet_size, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
index=(0, 0),
),
poolscanner.PoolConstraint(
b"Cbrb",
type_name=symbol_table + constants.BANG + "_GENERIC_CALLBACK",
size=(generic_callback_size, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
),
]
@staticmethod
def _create_scan_constraints_vista_and_above(
symbol_table: str,
) -> List[poolscanner.PoolConstraint]:
"""Creates a list of Pool Tag Constraints for callback objects.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
symbol_table: The name of an existing symbol table containing the symbols / types
Returns:
The list containing the built constraints.
"""
return [
poolscanner.PoolConstraint(
b"DbCb",
type_name=symbol_table + constants.BANG + "_DBGPRINT_CALLBACK",
size=(0x20, 0x40),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
),
poolscanner.PoolConstraint(
b"Pnp9",
type_name=symbol_table + constants.BANG + "_NOTIFY_ENTRY_HEADER",
size=(0x30, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
index=(1, 1),
),
poolscanner.PoolConstraint(
b"PnpD",
type_name=symbol_table + constants.BANG + "_NOTIFY_ENTRY_HEADER",
size=(0x40, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
index=(1, 1),
),
poolscanner.PoolConstraint(
b"PnpC",
type_name=symbol_table + constants.BANG + "_NOTIFY_ENTRY_HEADER",
size=(0x38, None),
page_type=poolscanner.PoolType.NONPAGED
| poolscanner.PoolType.PAGED
| poolscanner.PoolType.FREE,
index=(1, 1),
),
]
@classmethod
def create_callback_symbol_table(
cls,
context: interfaces.context.ContextInterface,
nt_symbol_table: str,
config_path: str,
) -> str:
"""Creates a symbol table for kernel callback objects.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
nt_symbol_table: The name of the table containing the kernel symbols
config_path: The config path where to find symbol files
Returns:
The name of the constructed symbol table
"""
native_types = context.symbol_space[nt_symbol_table].natives
is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table)
table_mapping = {"nt_symbols": nt_symbol_table}
if is_64bit:
symbol_filename = "callbacks-x64"
@@ -65,10 +200,143 @@ class Callbacks(interfaces.plugins.PluginInterface):
config_path,
"windows",
symbol_filename,
class_types=callbacks.class_types_x86 if not is_64bit else None,
native_types=native_types,
table_mapping=table_mapping,
)
@classmethod
def scan(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
nt_symbol_table: str,
callback_symbol_table: str,
) -> Iterable[
Tuple[
Union[str, interfaces.renderers.BaseAbsentValue],
int,
Union[str, interfaces.renderers.BaseAbsentValue],
]
]:
"""Scans for callback objects using the poolscanner module and constraints.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
nt_symbol_table: The name of the table containing the kernel symbols
callback_symbol_table: The name of the table containing the callback object symbols (_SHUTDOWN_PACKET etc.)
Returns:
A list of callback objects found by scanning the `layer_name` layer for callback pool signatures
"""
is_vista_or_later = versions.is_vista_or_later(
context=context, symbol_table=nt_symbol_table
)
type_map = handles.Handles.get_type_map(context, layer_name, nt_symbol_table)
constraints = cls.create_callback_scan_constraints(
context, callback_symbol_table, is_vista_or_later
)
for (
_constraint,
mem_object,
_header,
) in poolscanner.PoolScanner.generate_pool_scan(
context, layer_name, nt_symbol_table, constraints
):
try:
if hasattr(mem_object, "is_valid") and not mem_object.is_valid():
continue
yield cls._process_scanned_callback(mem_object, type_map)
except exceptions.InvalidAddressException:
continue
@classmethod
def _process_scanned_callback(
cls, memory_object: objects.StructType, type_map: Dict[int, str]
) -> Tuple[
Union[str, interfaces.renderers.BaseAbsentValue],
int,
Union[str, interfaces.renderers.BaseAbsentValue],
]:
symbol_table = memory_object.get_symbol_table_name()
type_name = memory_object.vol.type_name
if isinstance(memory_object, callbacks._SHUTDOWN_PACKET) or (
type_name == symbol_table + constants.BANG + "_SHUTDOWN_PACKET"
):
callback_type = "IoRegisterShutdownNotification"
try:
driver = memory_object.DeviceObject.DriverObject
index = driverirp.MAJOR_FUNCTIONS.index("IRP_MJ_SHUTDOWN")
callback_address = driver.MajorFunction[index]
details = driver.DriverName.String or renderers.UnparsableValue()
except exceptions.InvalidAddressException:
callback_address = memory_object.vol.offset
details = renderers.NotApplicableValue()
elif type_name == symbol_table + constants.BANG + "_NOTIFICATION_PACKET":
callback_type = "IoRegisterFsRegistrationChange"
callback_address = memory_object.NotificationRoutine
details = renderers.NotApplicableValue()
elif type_name == symbol_table + constants.BANG + "_NOTIFY_ENTRY_HEADER":
driver = (
memory_object.DriverObject.dereference()
if memory_object.DriverObject.is_readable()
else None
)
if driver:
# Instantiate an object header for the driver name
header = driver.get_object_header()
try:
if header.get_object_type(type_map) == "Driver":
# Grab the object name
details = header.NameInfo.Name.String
else:
details = renderers.NotApplicableValue()
except exceptions.InvalidAddressException:
details = renderers.UnreadableValue()
else:
details = renderers.UnreadableValue()
callback_type = (
memory_object.EventCategory.description
if memory_object.EventCategory.is_valid_choice
else renderers.UnparsableValue()
)
callback_address = memory_object.CallbackRoutine
elif type_name == symbol_table + constants.BANG + "_GENERIC_CALLBACK":
callback_type = "GenericKernelCallback"
callback_address = memory_object.Callback
details = renderers.NotApplicableValue()
elif type_name == symbol_table + constants.BANG + "_DBGPRINT_CALLBACK":
callback_type = "DbgSetDebugPrintCallback"
callback_address = memory_object.Function
details = renderers.NotApplicableValue()
else:
raise ValueError(f"Unexpected object type {type_name}")
return (
callback_type,
callback_address,
(
details
if not isinstance(details, interfaces.renderers.BaseAbsentValue)
else details
),
)
@classmethod
def list_notify_routines(
cls,
@@ -115,11 +383,14 @@ class Callbacks(interfaces.plugins.PluginInterface):
else:
count = 8
fast_refs = ntkrnlmp.object(
object_type="array",
offset=symbol_offset,
subtype=ntkrnlmp.get_type("_EX_FAST_REF"),
count=count,
fast_refs = cast(
List[objects.Pointer],
ntkrnlmp.object(
object_type="array",
offset=symbol_offset,
subtype=ntkrnlmp.get_type("_EX_FAST_REF"),
count=count,
),
)
for fast_ref in fast_refs:
@@ -159,11 +430,14 @@ class Callbacks(interfaces.plugins.PluginInterface):
if callback_count == 0:
return None
fast_refs = ntkrnlmp.object(
object_type="array",
offset=symbol_offset,
subtype=ntkrnlmp.get_type("_EX_FAST_REF"),
count=callback_count,
fast_refs = cast(
List[objects.Pointer],
ntkrnlmp.object(
object_type="array",
offset=symbol_offset,
subtype=ntkrnlmp.get_type("_EX_FAST_REF"),
count=callback_count,
),
)
for fast_ref in fast_refs:
@@ -265,7 +539,13 @@ class Callbacks(interfaces.plugins.PluginInterface):
layer_name: str,
symbol_table: str,
callback_table_name: str,
) -> Iterable[Tuple[str, int, str]]:
) -> Iterable[
Tuple[
str,
int,
interfaces.renderers.BaseAbsentValue,
]
]:
"""Lists all kernel bugcheck reason callbacks.
Args:
@@ -323,7 +603,13 @@ class Callbacks(interfaces.plugins.PluginInterface):
layer_name: str,
symbol_table: str,
callback_table_name: str,
) -> Iterable[Tuple[str, int, str]]:
) -> Iterable[
Tuple[
str,
int,
Union[interfaces.objects.ObjectInterface, renderers.UnreadableValue],
]
]:
"""Lists all kernel bugcheck callbacks.
Args:
@@ -372,7 +658,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
callback_table_name = self.create_callback_table(
callback_symbol_table = self.create_callback_symbol_table(
self.context, kernel.symbol_table_name, self.config_path
)
@@ -385,6 +671,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
self.list_bugcheck_callbacks,
self.list_bugcheck_reason_callbacks,
self.list_registry_callbacks,
self.scan,
)
for callback_method in callback_methods:
@@ -392,7 +679,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
self.context,
kernel.layer_name,
kernel.symbol_table_name,
callback_table_name,
callback_symbol_table,
):
if callback_detail is None:
detail = renderers.NotApplicableValue()
@@ -0,0 +1,211 @@
# 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
# Full details on the techniques used in these plugins to detect EDR-evading malware
# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation
# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf
import logging
from typing import Tuple, Optional, Generator, List, Dict
from functools import partial
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
import volatility3.plugins.windows.pslist as pslist
import volatility3.plugins.windows.threads as threads
import volatility3.plugins.windows.pe_symbols as pe_symbols
vollog = logging.getLogger(__name__)
class DebugRegisters(interfaces.plugins.PluginInterface):
# version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags
_required_framework_version = (2, 6, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0)
),
]
@staticmethod
def _get_debug_info(
ethread: interfaces.objects.ObjectInterface,
) -> Optional[Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]]:
"""
Gathers information related to the debug registers for the given thread
Args:
ethread: the thread (_ETHREAD) to examine
Returns:
Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]: The owner process of the thread and the values for dr7, dr0, dr1, dr2, dr3
"""
try:
dr7 = ethread.Tcb.TrapFrame.Dr7
state = ethread.Tcb.State
except exceptions.InvalidAddressException:
return None
# 0 = debug registers not active
# 4 = terminated
if dr7 == 0 or state == 4:
return None
try:
owner_proc = ethread.owning_process()
except (AttributeError, exceptions.InvalidAddressException):
return None
dr0 = ethread.Tcb.TrapFrame.Dr0
dr1 = ethread.Tcb.TrapFrame.Dr1
dr2 = ethread.Tcb.TrapFrame.Dr2
dr3 = ethread.Tcb.TrapFrame.Dr3
# bail if all are 0
if not (dr0 or dr1 or dr2 or dr3):
return None
return owner_proc, dr7, dr0, dr1, dr2, dr3
def _generator(
self,
) -> Generator[
Tuple[
int,
Tuple[
str,
int,
int,
int,
int,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
],
],
None,
None,
]:
kernel = self.context.modules[self.config["kernel"]]
vads_cache: Dict[int, pe_symbols.ranges_type] = {}
proc_modules = None
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
)
for proc in procs:
for thread in threads.Threads.list_threads(kernel, proc):
debug_info = self._get_debug_info(thread)
if not debug_info:
continue
owner_proc, dr7, dr0, dr1, dr2, dr3 = debug_info
vads = pe_symbols.PESymbols.get_vads_for_process_cache(
vads_cache, owner_proc
)
if not vads:
continue
# this lookup takes a while, so only perform if we need to
if not proc_modules:
proc_modules = pe_symbols.PESymbols.get_process_modules(
self.context, kernel.layer_name, kernel.symbol_table_name, None
)
path_and_symbol = partial(
pe_symbols.PESymbols.path_and_symbol_for_address,
self.context,
self.config_path,
proc_modules,
)
file0, sym0 = path_and_symbol(vads, dr0)
file1, sym1 = path_and_symbol(vads, dr1)
file2, sym2 = path_and_symbol(vads, dr2)
file3, sym3 = path_and_symbol(vads, dr3)
# if none map to an actual file VAD then bail
if not (file0 or file1 or file2 or file3):
continue
process_name = owner_proc.ImageFileName.cast(
"string",
max_length=owner_proc.ImageFileName.vol.count,
errors="replace",
)
thread_tid = thread.Cid.UniqueThread
yield (
0,
(
process_name,
owner_proc.UniqueProcessId,
thread_tid,
thread.Tcb.State,
dr7,
format_hints.Hex(dr0),
file0 or renderers.NotApplicableValue(),
sym0 or renderers.NotApplicableValue(),
format_hints.Hex(dr1),
file1 or renderers.NotApplicableValue(),
sym1 or renderers.NotApplicableValue(),
format_hints.Hex(dr2),
file2 or renderers.NotApplicableValue(),
sym2 or renderers.NotApplicableValue(),
format_hints.Hex(dr3),
file3 or renderers.NotApplicableValue(),
sym3 or renderers.NotApplicableValue(),
),
)
def run(self) -> renderers.TreeGrid:
return renderers.TreeGrid(
[
("Process", str),
("PID", int),
("TID", int),
("State", int),
("Dr7", int),
("Dr0", format_hints.Hex),
("Range0", str),
("Symbol0", str),
("Dr1", format_hints.Hex),
("Range1", str),
("Symbol1", str),
("Dr2", format_hints.Hex),
("Range2", str),
("Symbol2", str),
("Dr3", format_hints.Hex),
("Range3", str),
("Symbol3", str),
],
self._generator(),
)
@@ -1,10 +1,10 @@
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
# 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
#
import contextlib
import datetime
import logging
import ntpath
import re
from typing import List, Optional, Type
from volatility3.framework import constants, exceptions, interfaces, renderers
@@ -13,7 +13,7 @@ from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins import timeliner
from volatility3.plugins.windows import info, pslist, psscan
from volatility3.plugins.windows import info, pslist, psscan, pedump
vollog = logging.getLogger(__name__)
@@ -22,7 +22,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the loaded modules in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (3, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -53,73 +53,33 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
description="Process offset in the physical address space",
optional=True,
),
requirements.StringRequirement(
name="name",
description="Specify a regular expression to match dll name(s)",
optional=True,
),
requirements.IntRequirement(
name="base",
description="Specify a base virtual address in process memory",
optional=True,
),
requirements.BooleanRequirement(
name="ignore-case",
description="Specify case insensitivity for the regular expression name matching",
default=False,
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed DLLs",
default=False,
optional=True,
),
requirements.VersionRequirement(
name="pedump", component=pedump.PEDump, version=(1, 0, 0)
),
]
@classmethod
def dump_pe(
cls,
context: interfaces.context.ContextInterface,
pe_table_name: str,
dll_entry: interfaces.objects.ObjectInterface,
open_method: Type[interfaces.plugins.FileHandlerInterface],
layer_name: str = None,
prefix: str = "",
) -> Optional[interfaces.plugins.FileHandlerInterface]:
"""Extracts the complete data for a process as a FileInterface
Args:
context: the context to operate upon
pe_table_name: the name for the symbol table containing the PE format symbols
dll_entry: the object representing the module
layer_name: the layer that the DLL lives within
open_method: class for constructing output files
Returns:
An open FileHandlerInterface object containing the complete data for the DLL or None in the case of failure
"""
try:
try:
name = dll_entry.FullDllName.get_string()
except exceptions.InvalidAddressException:
name = "UnreadableDLLName"
if layer_name is None:
layer_name = dll_entry.vol.layer_name
file_handle = open_method(
"{}{}.{:#x}.{:#x}.dmp".format(
prefix,
ntpath.basename(name),
dll_entry.vol.offset,
dll_entry.DllBase,
)
)
dos_header = context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=dll_entry.DllBase,
layer_name=layer_name,
)
for offset, data in dos_header.reconstruct():
file_handle.seek(offset)
file_handle.write(data)
except (
IOError,
exceptions.VolatilityException,
OverflowError,
ValueError,
) as excp:
vollog.debug(f"Unable to dump dll at offset {dll_entry.DllBase}: {excp}")
return None
return file_handle
def _generator(self, procs):
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
@@ -144,9 +104,35 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
BaseDllName = FullDllName = renderers.UnreadableValue()
with contextlib.suppress(exceptions.InvalidAddressException):
BaseDllName = entry.BaseDllName.get_string()
# We assume that if the BaseDllName points to an invalid buffer, so will FullDllName
# We assume that if BaseDllName points to invalid buffer, so will FullDllName
FullDllName = entry.FullDllName.get_string()
# Check if a name regex was passed and apply it to only show matches
if self.config["name"]:
try:
flags = re.I if self.config["ignore-case"] else 0
mod_re = re.compile(self.config["name"], flags)
except re.error:
vollog.debug(
"Error parsing regular expression: %s", self.config["name"]
)
return None
# If Base or Full Dll Name are invalid, move on
if isinstance(BaseDllName, renderers.UnreadableValue) or isinstance(
FullDllName, renderers.UnreadableValue
):
continue
# If regex does not match, move on
if not mod_re.search(BaseDllName) and not mod_re.search(
FullDllName
):
continue
if self.config["base"] and self.config["base"] != entry.DllBase:
continue
if dll_load_time_field:
# Versions prior to 6.1 won't have the LoadTime attribute
# and 32bit version shouldn't have the Quadpart according to MSDN
@@ -161,7 +147,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
file_output = "Disabled"
if self.config["dump"]:
file_handle = self.dump_pe(
file_output = pedump.PEDump.dump_ldr_entry(
self.context,
pe_table_name,
entry,
@@ -169,10 +155,10 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
proc_layer_name,
prefix=f"pid.{proc_id}.",
)
file_output = "Error outputting file"
if file_handle:
file_handle.close()
file_output = file_handle.preferred_filename
if not file_output:
file_output = "Error outputting file"
try:
dllbase = format_hints.Hex(entry.DllBase)
except exceptions.InvalidAddressException:
@@ -44,6 +44,7 @@ class DriverIrp(interfaces.plugins.PluginInterface):
"""List IRPs for drivers in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
@@ -244,6 +244,8 @@ class DumpFiles(interfaces.plugins.PluginInterface):
symbol_table=kernel.symbol_table_name,
)
dumped_files = set()
for proc in procs:
try:
object_table = proc.ObjectTable
@@ -267,6 +269,10 @@ class DumpFiles(interfaces.plugins.PluginInterface):
if not file_re.search(name):
continue
if file_obj.vol.offset in dumped_files:
continue
dumped_files.add(file_obj.vol.offset)
for result in self.process_file_object(
self.context, kernel.layer_name, self.open, file_obj
):
@@ -303,6 +309,10 @@ class DumpFiles(interfaces.plugins.PluginInterface):
if not file_re.search(name):
continue
if file_obj.vol.offset in dumped_files:
continue
dumped_files.add(file_obj.vol.offset)
for result in self.process_file_object(
self.context, kernel.layer_name, self.open, file_obj
):
@@ -14,6 +14,7 @@ class FileScan(interfaces.plugins.PluginInterface):
"""Scans for file objects present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -67,10 +68,10 @@ class FileScan(interfaces.plugins.PluginInterface):
except exceptions.InvalidAddressException:
continue
yield (0, (format_hints.Hex(fileobj.vol.offset), file_name, fileobj.Size))
yield (0, (format_hints.Hex(fileobj.vol.offset), file_name))
def run(self):
return renderers.TreeGrid(
[("Offset", format_hints.Hex), ("Name", str), ("Size", int)],
[("Offset", format_hints.Hex), ("Name", str)],
self._generator(),
)
@@ -25,7 +25,7 @@ class Handles(interfaces.plugins.PluginInterface):
"""Lists process open handles."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 2)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -142,9 +142,13 @@ class Handles(interfaces.plugins.PluginInterface):
pointers in the _HANDLE_TABLE_ENTRY which allows us to find the
associated _OBJECT_HEADER.
"""
DEFAULT_SAR_VALUE = 0x10 # to be used only when decoding fails
if self._sar_value is None:
if not has_capstone:
vollog.debug(
"capstone module is missing, unable to create disassembly of ObpCaptureHandleInformationEx"
)
return None
kernel = self.context.modules[self.config["kernel"]]
@@ -159,25 +163,48 @@ class Handles(interfaces.plugins.PluginInterface):
try:
func_addr = ntkrnlmp.get_symbol("ObpCaptureHandleInformationEx").address
except exceptions.SymbolError:
vollog.debug("Unable to locate ObpCaptureHandleInformationEx symbol")
return None
data = self.context.layers.read(virtual_layer_name, kvo + func_addr, 0x200)
if data is None:
return None
try:
func_addr_to_read = kvo + func_addr
num_bytes_to_read = 0x200
vollog.debug(
f"ObpCaptureHandleInformationEx symbol located at {hex(func_addr_to_read)}"
)
data = self.context.layers.read(
virtual_layer_name, func_addr_to_read, num_bytes_to_read
)
except exceptions.InvalidAddressException:
vollog.warning(
f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}. Unable to decode SAR value. Failing back to a common value of {hex(DEFAULT_SAR_VALUE)}"
)
self._sar_value = DEFAULT_SAR_VALUE
return self._sar_value
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
instruction_count = 0
for address, size, mnemonic, op_str in md.disasm_lite(
data, kvo + func_addr
):
# print("{} {} {} {}".format(address, size, mnemonic, op_str))
instruction_count += 1
if mnemonic.startswith("sar"):
# if we don't want to parse op strings, we can disasm the
# single sar instruction again, but we use disasm_lite for speed
self._sar_value = int(op_str.split(",")[1].strip(), 16)
vollog.debug(
f"SAR located at {hex(address)} with value of {hex(self._sar_value)}"
)
break
if self._sar_value is None:
vollog.warning(
f"Failed to to locate SAR value having parsed {instruction_count} instructions, failing back to a common value of {hex(DEFAULT_SAR_VALUE)}"
)
self._sar_value = DEFAULT_SAR_VALUE
return self._sar_value
@classmethod
@@ -0,0 +1,237 @@
# 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
#
import logging
from typing import NamedTuple, Dict, Generator
from volatility3.framework import interfaces, exceptions, constants
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.plugins.windows import pslist, vadinfo
vollog = logging.getLogger(__name__)
VadData = NamedTuple(
"VadData",
[
("protection", str),
("path", str),
],
)
DLLData = NamedTuple(
"DLLData",
[
("path", str),
],
)
### Useful references on process hollowing
# https://cysinfo.com/detecting-deceptive-hollowing-techniques/
# https://github.com/m0n0ph1/Process-Hollowing
class HollowProcesses(interfaces.plugins.PluginInterface):
"""Lists hollowed processes"""
_required_framework_version = (2, 4, 0)
@classmethod
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.ListRequirement(
name="pid",
element_type=int,
description="Process IDs to include (all other processes are excluded)",
optional=True,
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0)
),
]
def _get_vads_data(
self, proc: interfaces.objects.ObjectInterface
) -> Dict[int, VadData]:
"""
Returns a dictionary of:
base address -> (protection string, file name)
For each mapped VAD in the process. This is used
for quick lookups of data and matching the DLL
at the same base address as the VAD
"""
vads = {}
kernel = self.context.modules[self.config["kernel"]]
for vad in proc.get_vad_root().traverse():
protection_string = vad.get_protection(
vadinfo.VadInfo.protect_values(
self.context, kernel.layer_name, kernel.symbol_table_name
),
vadinfo.winnt_protections,
)
fn = vad.get_file_name()
if not fn or not isinstance(fn, str):
fn = "<Non-File Backed Region>"
vads[vad.get_start()] = VadData(protection_string, fn)
return vads
def _get_dlls_map(
self, proc: interfaces.objects.ObjectInterface
) -> Dict[int, DLLData]:
"""
Returns a dictionary of:
base address -> path
for each DLL loaded in the process
This is used to cross compare with
the corresponding VAD and to have a
backup path source in case of smear
in the VAD
"""
dlls = {}
for entry in proc.load_order_modules():
try:
base = entry.DllBase
except exceptions.InvalidAddressException:
continue
try:
FullDllName = entry.FullDllName.get_string()
except exceptions.InvalidAddressException:
FullDllName = renderers.UnreadableValue()
dlls[base] = DLLData(FullDllName)
return dlls
def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int:
"""
Uses the PEB to get the image base of the process
"""
kernel = self.context.modules[self.config["kernel"]]
try:
proc_layer_name = proc.add_process_layer()
peb = self.context.object(
kernel.symbol_table_name + constants.BANG + "_PEB",
layer_name=proc_layer_name,
offset=proc.Peb,
)
return peb.ImageBaseAddress
except exceptions.InvalidAddressException:
return None
def _check_load_address(self, proc, _, __) -> Generator[str, None, None]:
"""
Detects when the image base in the PEB, which is writable by process malware,
does not match the section base address - whose value lives in kernel memory.
Many malware samples will manipulate their image base to fool AVs/EDRs and
as a necessary part of certain hollowing techniques
"""
image_base = self._get_image_base(proc)
if image_base is not None and image_base != proc.SectionBaseAddress:
yield "The ImageBaseAddress reported from the PEB ({:#x}) does not match the process SectionBaseAddress ({:#x})".format(
image_base, proc.SectionBaseAddress
)
def _check_exe_protection(
self, proc, vads: Dict[int, VadData], __
) -> Generator[str, None, None]:
"""
Legitimately mapped application executables and DLLs
will have a VAD present and its initial protection will be
PAGE_EXECUTE_WRITECOPY.
Many process hollowing and code injection techniques will
unmap the real executable and/or map in executables with
incorrect permissions.
This check verifies the VAD for the application exe.
`_check_dlls_protection` checks for DLLs mapped in the process.
"""
base = proc.SectionBaseAddress
if base not in vads:
yield "There is no VAD starting at the base address of the process executable ({:#x})".format(
base
)
elif vads[base].protection != "PAGE_EXECUTE_WRITECOPY":
yield "Unexpected protection ({}) for VAD hosting the process executable ({:#x}) with path {}".format(
vads[base].protection, base, vads[base].path
)
def _check_dlls_protection(
self, _, vads: Dict[int, VadData], dlls: Dict[int, DLLData]
) -> Generator[str, None, None]:
for dll_base in dlls:
# could be malicious but triggers too many FPs from smear
if dll_base not in vads:
continue
# PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files
if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY":
yield "Unexpected protection ({}) for DLL in the PEB's load order list ({:#x}) with path {}".format(
vads[dll_base].protection, dll_base, dlls[dll_base].path
)
def _generator(self, procs):
checks = [
self._check_load_address,
self._check_exe_protection,
self._check_dlls_protection,
]
for proc in procs:
# smear and/or terminated process
dlls = self._get_dlls_map(proc)
if len(dlls) < 3:
continue
vads = self._get_vads_data(proc)
if len(vads) < 5:
continue
proc_name = utility.array_to_string(proc.ImageFileName)
pid = proc.UniqueProcessId
for check in checks:
for note in check(proc, vads, dlls):
yield 0, (
pid,
proc_name,
note,
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
kernel = self.context.modules[self.config["kernel"]]
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Notes", str),
],
self._generator(
pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
),
)
@@ -10,7 +10,7 @@ from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.renderers import TreeGrid
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import extensions
from volatility3.framework.symbols.windows.extensions import kdbg, pe
class Info(plugins.PluginInterface):
@@ -94,16 +94,16 @@ class Info(plugins.PluginInterface):
"windows",
"kdbg",
native_types=native_types,
class_types=extensions.kdbg.class_types,
class_types=kdbg.class_types,
)
kdbg = context.object(
kdbg_obj = context.object(
kdbg_table_name + constants.BANG + "_KDDEBUGGER_DATA64",
offset=ntkrnlmp.offset + kdbg_offset,
layer_name=layer_name,
)
return kdbg
return kdbg_obj
@classmethod
def get_kuser_structure(
@@ -173,7 +173,7 @@ class Info(plugins.PluginInterface):
interfaces.configuration.path_join(config_path, "pe"),
"windows",
"pe",
class_types=extensions.pe.class_types,
class_types=pe.class_types,
)
dos_header = context.object(
@@ -0,0 +1,106 @@
# 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
#
import logging
from typing import Iterator, List, Tuple
from volatility3.framework import (
renderers,
interfaces,
constants,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
vollog = logging.getLogger(__name__)
class KPCRs(interfaces.plugins.PluginInterface):
"""Print KPCR structure for each processor"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def list_kpcrs(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
layer_name: str,
symbol_table: str,
) -> interfaces.objects.ObjectInterface:
"""Returns the KPCR structure for each processor
Args:
context: The context to retrieve required elements (layers, symbol tables) from
kernel_module_name: The name of the kernel module on which to operate
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
Returns:
The _KPCR structure for each processor
"""
kernel = context.modules[kernel_module_name]
cpu_count_offset = kernel.get_symbol("KeNumberProcessors").address
cpu_count = kernel.object(
object_type="unsigned int", layer_name=layer_name, offset=cpu_count_offset
)
processor_block = kernel.object(
object_type="pointer",
layer_name=layer_name,
offset=kernel.get_symbol("KiProcessorBlock").address,
)
processor_pointers = utility.array_of_pointers(
context=context,
array=processor_block,
count=cpu_count,
subtype=symbol_table + constants.BANG + "_KPRCB",
)
for pointer in processor_pointers:
kprcb = pointer.dereference()
reloff = kernel.get_type("_KPCR").relative_child_offset("Prcb")
kpcr = context.object(
symbol_table + constants.BANG + "_KPCR",
offset=kprcb.vol.offset - reloff,
layer_name=layer_name,
)
yield kpcr
def _generator(self) -> Iterator[Tuple]:
kernel = self.context.modules[self.config["kernel"]]
layer_name = kernel.layer_name
symbol_table = kernel.symbol_table_name
for kpcr in self.list_kpcrs(
self.context, self.config["kernel"], layer_name, symbol_table
):
yield (
0,
(
format_hints.Hex(kpcr.vol.offset),
format_hints.Hex(kpcr.CurrentPrcb),
),
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("PRCB Offset", format_hints.Hex),
],
self._generator(),
)
@@ -1,3 +1,9 @@
# 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
#
import logging
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -5,12 +11,14 @@ from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import pslist, vadinfo
vollog = logging.getLogger(__name__)
class LdrModules(interfaces.plugins.PluginInterface):
"""Lists the loaded modules in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -39,14 +47,6 @@ class LdrModules(interfaces.plugins.PluginInterface):
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
)
def filter_function(x: interfaces.objects.ObjectInterface) -> bool:
try:
return not (x.get_private_memory() == 0 and x.ControlArea)
except AttributeError:
return False
filter_func = filter_function
for proc in procs:
proc_layer_name = proc.add_process_layer()
@@ -61,7 +61,7 @@ class LdrModules(interfaces.plugins.PluginInterface):
# Build dictionary of mapped files, where the VAD start address is the key and value is the file name of the mapped file
mapped_files = {}
for vad in vadinfo.VadInfo.list_vads(proc, filter_func=filter_func):
for vad in vadinfo.VadInfo.list_vads(proc):
dos_header = self.context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=vad.get_start(),
@@ -71,7 +71,11 @@ class LdrModules(interfaces.plugins.PluginInterface):
# Filter out VADs that do not start with a MZ header
if dos_header.e_magic != 0x5A4D:
continue
except exceptions.PagedInvalidAddressException:
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping vad at {hex(dos_header.vol.offset)} due to InvalidAddressException",
)
continue
mapped_files[vad.get_start()] = vad.get_file_name()
@@ -155,12 +155,12 @@ class Malfind(interfaces.plugins.PluginInterface):
for proc in procs:
# by default, "Notes" column will be set to N/A
notes = renderers.NotApplicableValue()
process_name = utility.array_to_string(proc.ImageFileName)
for vad, data in self.list_injections(
self.context, kernel.layer_name, kernel.symbol_table_name, proc
):
notes = renderers.NotApplicableValue()
# Check for unique headers and update "Notes" column if criteria is met
if data[0:2] in refined_criteria:
notes = refined_criteria[data[0:2]]
@@ -29,7 +29,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0)
),
]
@@ -38,7 +38,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
{"yara_string": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -197,7 +197,7 @@ class ADS(interfaces.plugins.PluginInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
{"yara_string": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -264,9 +264,10 @@ class ADS(interfaces.plugins.PluginInterface):
disasm = interfaces.renderers.Disassembly(
content, 0, architecture.lower()
)
content = format_hints.HexBytes(content)
else:
content = renderers.NotAvailableValue
disasm = interfaces.renderers.BaseAbsentValue
content = renderers.NotAvailableValue()
disasm = interfaces.renderers.BaseAbsentValue()
yield 0, (
format_hints.Hex(attr_data.vol.offset),
@@ -275,7 +276,7 @@ class ADS(interfaces.plugins.PluginInterface):
attr.Attr_Header.AttrType.lookup(),
file_name,
ads_name,
format_hints.HexBytes(content),
content,
disasm,
)
else:
+24 -166
View File
@@ -2,23 +2,24 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import Iterable, List, Generator
from typing import Iterable
from volatility3.framework import renderers, interfaces, exceptions, constants
from volatility3.framework import interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import poolscanner, dlllist, pslist
from volatility3.plugins.windows import poolscanner, modules, pedump
vollog = logging.getLogger(__name__)
class ModScan(interfaces.plugins.PluginInterface):
class ModScan(modules.Modules):
"""Scans for modules present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (2, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enumeration_method = self.scan_modules
@classmethod
def get_requirements(cls):
@@ -32,10 +33,7 @@ class ModScan(interfaces.plugins.PluginInterface):
name="poolscanner", component=poolscanner.PoolScanner, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="dlllist", component=dlllist.DllList, version=(2, 0, 0)
name="modules", component=modules.Modules, version=(2, 0, 0)
),
requirements.BooleanRequirement(
name="dump",
@@ -43,6 +41,20 @@ class ModScan(interfaces.plugins.PluginInterface):
default=False,
optional=True,
),
requirements.IntRequirement(
name="base",
description="Extract a single module with BASE address",
optional=True,
),
requirements.StringRequirement(
name="name",
description="module name/sub string",
optional=True,
default=None,
),
requirements.VersionRequirement(
name="pedump", component=pedump.PEDump, version=(1, 0, 0)
),
]
@classmethod
@@ -72,157 +84,3 @@ class ModScan(interfaces.plugins.PluginInterface):
):
_constraint, mem_object, _header = result
yield mem_object
@classmethod
def get_session_layers(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
pids: List[int] = None,
) -> Generator[str, None, None]:
"""Build a cache of possible virtual layers, in priority starting with
the primary/kernel layer. Then keep one layer per session by cycling
through the process list.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
pids: A list of process identifiers to include exclusively or None for no filter
Returns:
A list of session layer names
"""
seen_ids: List[interfaces.objects.ObjectInterface] = []
filter_func = pslist.PsList.create_pid_filter(pids or [])
for proc in pslist.PsList.list_processes(
context=context,
layer_name=layer_name,
symbol_table=symbol_table,
filter_func=filter_func,
):
proc_id = "Unknown"
try:
proc_id = proc.UniqueProcessId
proc_layer_name = proc.add_process_layer()
# create the session space object in the process' own layer.
# not all processes have a valid session pointer.
session_space = context.object(
symbol_table + constants.BANG + "_MM_SESSION_SPACE",
layer_name=layer_name,
offset=proc.Session,
)
if session_space.SessionId in seen_ids:
continue
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVV,
"Process {} does not have a valid Session or a layer could not be constructed for it".format(
proc_id
),
)
continue
# save the layer if we haven't seen the session yet
seen_ids.append(session_space.SessionId)
yield proc_layer_name
@classmethod
def find_session_layer(
cls,
context: interfaces.context.ContextInterface,
session_layers: Iterable[str],
base_address: int,
):
"""Given a base address and a list of layer names, find a layer that
can access the specified address.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
session_layers: A list of session layer names
base_address: The base address to identify the layers that can access it
Returns:
Layer name or None if no layers that contain the base address can be found
"""
for layer_name in session_layers:
if context.layers[layer_name].is_valid(base_address):
return layer_name
return None
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
session_layers = list(
self.get_session_layers(
self.context, kernel.layer_name, kernel.symbol_table_name
)
)
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
)
for mod in self.scan_modules(
self.context, kernel.layer_name, kernel.symbol_table_name
):
try:
BaseDllName = mod.BaseDllName.get_string()
except exceptions.InvalidAddressException:
BaseDllName = ""
try:
FullDllName = mod.FullDllName.get_string()
except exceptions.InvalidAddressException:
FullDllName = ""
file_output = "Disabled"
if self.config["dump"]:
session_layer_name = self.find_session_layer(
self.context, session_layers, mod.DllBase
)
file_output = f"Cannot find a viable session layer for {mod.DllBase:#x}"
if session_layer_name:
file_handle = dlllist.DllList.dump_pe(
self.context,
pe_table_name,
mod,
self.open,
layer_name=session_layer_name,
)
file_output = "Error outputting file"
if file_handle:
file_output = file_handle.preferred_filename
yield (
0,
(
format_hints.Hex(mod.vol.offset),
format_hints.Hex(mod.DllBase),
format_hints.Hex(mod.SizeOfImage),
BaseDllName,
FullDllName,
file_output,
),
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str),
("File output", str),
],
self._generator(),
)
@@ -4,14 +4,12 @@
import logging
from typing import List, Iterable, Generator
from volatility3.framework import constants
from volatility3.framework import exceptions, interfaces
from volatility3.framework import renderers
from volatility3.framework import exceptions, interfaces, constants, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import pslist, dlllist
from volatility3.plugins.windows import pslist, pedump
vollog = logging.getLogger(__name__)
@@ -20,7 +18,11 @@ class Modules(interfaces.plugins.PluginInterface):
"""Lists the loaded kernel modules."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
_version = (2, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enumeration_method = self.list_modules
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -33,65 +35,97 @@ class Modules(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="dlllist", component=dlllist.DllList, version=(2, 0, 0)
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed modules",
default=False,
optional=True,
),
requirements.IntRequirement(
name="base",
description="Extract a single module with BASE address",
optional=True,
),
requirements.StringRequirement(
name="name",
description="module name/sub string",
optional=True,
default=None,
),
requirements.VersionRequirement(
name="pedump", component=pedump.PEDump, version=(1, 0, 0)
),
]
def dump_module(self, session_layers, pe_table_name, mod):
session_layer_name = self.find_session_layer(
self.context, session_layers, mod.DllBase
)
file_output = f"Cannot find a viable session layer for {mod.DllBase:#x}"
if session_layer_name:
file_output = pedump.PEDump.dump_ldr_entry(
self.context,
pe_table_name,
mod,
self.open,
layer_name=session_layer_name,
)
if not file_output:
file_output = "Error outputting file"
return file_output
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
)
for mod in self.list_modules(
pe_table_name = None
session_layers = None
if self.config["dump"]:
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"windows",
"pe",
class_types=pe.class_types,
)
session_layers = list(
self.get_session_layers(
self.context, kernel.layer_name, kernel.symbol_table_name
)
)
for mod in self._enumeration_method(
self.context, kernel.layer_name, kernel.symbol_table_name
):
if self.config["base"] and self.config["base"] != mod.DllBase:
continue
try:
BaseDllName = mod.BaseDllName.get_string()
except exceptions.InvalidAddressException:
BaseDllName = ""
try:
FullDllName = mod.FullDllName.get_string()
except exceptions.InvalidAddressException:
FullDllName = ""
BaseDllName = interfaces.renderers.BaseAbsentValue()
if self.config["name"] and self.config["name"] not in BaseDllName:
continue
try:
FullDllName = mod.FullDllName.get_string()
except exceptions.InvalidAddressException:
FullDllName = interfaces.renderers.BaseAbsentValue()
file_output = "Disabled"
if self.config["dump"]:
file_handle = dlllist.DllList.dump_pe(
self.context, pe_table_name, mod, self.open
)
file_output = "Error outputting file"
if file_handle:
file_handle.close()
file_output = file_handle.preferred_filename
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
@@ -218,6 +218,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
(10, 0, 18362, 0): "netscan-win10-18362-x64",
(10, 0, 18363, 0): "netscan-win10-18363-x64",
(10, 0, 19041, 0): "netscan-win10-19041-x64",
(10, 0, 20348, 0): "netscan-win10-20348-x64",
}
# we do not need to check for tcpip's specific FileVersion in every case
@@ -35,7 +35,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
name="netscan", component=netscan.NetScan, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="modules", component=modules.Modules, version=(1, 0, 0)
name="modules", component=modules.Modules, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0)
@@ -0,0 +1,96 @@
# 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
#
import logging
from typing import List, Generator
from volatility3.framework import interfaces, symbols
from volatility3.framework.configuration import requirements
from volatility3.plugins.windows import thrdscan, ssdt
vollog = logging.getLogger(__name__)
class Threads(thrdscan.ThrdScan):
"""Lists process threads"""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.implementation = self.list_orphan_kernel_threads
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="thrdscan", plugin=thrdscan.ThrdScan, version=(1, 1, 0)
),
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
]
@classmethod
def list_orphan_kernel_threads(
cls,
context: interfaces.context.ContextInterface,
module_name: str,
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Yields thread objects of kernel threads that do not map to a module
Args:
cls
context: the context to operate upon
module_name: name of the module to use for scanning
Returns:
A generator of thread objects of orphaned threads
"""
module = context.modules[module_name]
layer_name = module.layer_name
symbol_table = module.symbol_table_name
collection = ssdt.SSDT.build_module_collection(
context, layer_name, symbol_table
)
# FIXME - use a proper constant once established
# used to filter out smeared pointers
if symbols.symbol_table_is_64bit(context, symbol_table):
kernel_start = 0xFFFFF80000000000
else:
kernel_start = 0x80000000
for thread in thrdscan.ThrdScan.scan_threads(context, module_name):
# we don't want smeared or terminated threads
try:
proc = thread.owning_process()
except AttributeError:
continue
# we only care about kernel threads, 4 = System
# previous methods for determining if a thread was a kernel thread
# such as bit fields and flags are not stable in Win10+
# so we check if the thread is from the kernel itself or one its child
# kernel processes (MemCompression, Regsitry, ...)
if proc.UniqueProcessId != 4 and proc.InheritedFromUniqueProcessId != 4:
continue
if thread.StartAddress < kernel_start:
continue
module_symbols = list(
collection.get_module_symbols_by_absolute_location(thread.StartAddress)
)
# alert on threads that do not map to a module
if not module_symbols:
yield thread
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,270 @@
# 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
#
import logging
import ntpath
from typing import List, Type, Optional
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import pslist, modules
vollog = logging.getLogger(__name__)
class PEDump(interfaces.plugins.PluginInterface):
"""Allows extracting PE Files from a specific address in a specific address space"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
element_type=int,
description="Process IDs to include (all other processes are excluded)",
optional=True,
),
requirements.IntRequirement(
name="base",
description="Base address to reconstruct a PE file",
optional=False,
),
requirements.BooleanRequirement(
name="kernel_module",
description="Extract from kernel address space.",
default=False,
optional=True,
),
]
@classmethod
def dump_pe(
cls,
context: interfaces.context.ContextInterface,
pe_table_name: str,
layer_name: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
file_name: str,
base: int,
) -> Optional[str]:
"""
Returns the filename of the dump file or None
"""
try:
file_handle = open_method(file_name)
dos_header = context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=base,
layer_name=layer_name,
)
for offset, data in dos_header.reconstruct():
file_handle.seek(offset)
file_handle.write(data)
except (
IOError,
exceptions.VolatilityException,
OverflowError,
ValueError,
) as excp:
vollog.debug(f"Unable to dump PE file at offset {base}: {excp}")
return None
finally:
file_handle.close()
return file_handle.preferred_filename
@classmethod
def dump_ldr_entry(
cls,
context: interfaces.context.ContextInterface,
pe_table_name: str,
ldr_entry: interfaces.objects.ObjectInterface,
open_method: Type[interfaces.plugins.FileHandlerInterface],
layer_name: str = None,
prefix: str = "",
) -> Optional[str]:
"""Extracts the PE file referenced an LDR_DATA_TABLE_ENTRY (DLL, kernel module) instance
Args:
context: the context to operate upon
pe_table_name: the name for the symbol table containing the PE format symbols
ldr_entry: the object representing the module
open_method: class for constructing output files
layer_name: the layer that the DLL lives within
prefix: optional string to prepend to filename
Returns:
The output file name or None in the case of failure
"""
try:
name = ldr_entry.FullDllName.get_string()
except exceptions.InvalidAddressException:
name = "UnreadableDLLName"
if layer_name is None:
layer_name = ldr_entry.vol.layer_name
file_name = "{}{}.{:#x}.{:#x}.dmp".format(
prefix,
ntpath.basename(name),
ldr_entry.vol.offset,
ldr_entry.DllBase,
)
return cls.dump_pe(
context,
pe_table_name,
layer_name,
open_method,
file_name,
ldr_entry.DllBase,
)
@classmethod
def dump_pe_at_base(
cls,
context: interfaces.context.ContextInterface,
pe_table_name: str,
layer_name: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
proc_offset: int,
pid: int,
base: int,
) -> Optional[str]:
file_name = "PE.{:#x}.{:d}.{:#x}.dmp".format(
proc_offset,
pid,
base,
)
return PEDump.dump_pe(
context, pe_table_name, layer_name, open_method, file_name, base
)
@classmethod
def dump_kernel_pe_at_base(cls, context, kernel, pe_table_name, open_method, base):
session_layers = modules.Modules.get_session_layers(
context, kernel.layer_name, kernel.symbol_table_name
)
session_layer_name = modules.Modules.find_session_layer(
context, session_layers, base
)
if session_layer_name:
system_pid = 4
file_output = PEDump.dump_pe_at_base(
context,
pe_table_name,
session_layer_name,
open_method,
0,
system_pid,
base,
)
if file_output:
yield system_pid, "Kernel", file_output
else:
vollog.warning(
"Unable to find a session layer with the provided base address mapped in the kernel."
)
@classmethod
def dump_processes(
cls, context, kernel, pe_table_name, open_method, filter_func, base
):
""" """
for proc in pslist.PsList.list_processes(
context=context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
):
pid = proc.UniqueProcessId
proc_name = proc.ImageFileName.cast(
"string",
max_length=proc.ImageFileName.vol.count,
errors="replace",
)
proc_layer_name = proc.add_process_layer()
file_output = PEDump.dump_pe_at_base(
context,
pe_table_name,
proc_layer_name,
open_method,
proc.vol.offset,
pid,
base,
)
if file_output:
yield pid, proc_name, file_output
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
pe_table_name = intermed.IntermediateSymbolTable.create(
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
)
if self.config["kernel_module"] and self.config["pid"]:
vollog.error("Only --kernel_module or --pid should be set. Not both")
return
if not self.config["kernel_module"] and not self.config["pid"]:
vollog.error("--kernel_module or --pid must be set")
return
if self.config["kernel_module"]:
pe_files = self.dump_kernel_pe_at_base(
self.context, kernel, pe_table_name, self.open, self.config["base"]
)
else:
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
pe_files = self.dump_processes(
self.context,
kernel,
pe_table_name,
self.open,
filter_func,
self.config["base"],
)
for pid, proc_name, file_output in pe_files:
yield (
0,
(
pid,
proc_name,
file_output,
),
)
def run(self):
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("File output", str),
],
self._generator(),
)
@@ -0,0 +1,104 @@
# 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
#
import logging
import contextlib
from volatility3.framework import interfaces, exceptions
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import pslist
vollog = logging.getLogger(__name__)
class ProcessGhosting(interfaces.plugins.PluginInterface):
"""Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0"""
_required_framework_version = (2, 4, 0)
@classmethod
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
]
def _generator(self, procs):
kernel = self.context.modules[self.config["kernel"]]
if not kernel.get_type("_EPROCESS").has_member("ImageFilePointer"):
vollog.warning(
"This plugin only supports Windows 10 builds when the ImageFilePointer member of _EPROCESS is present"
)
return
for proc in procs:
delete_pending = renderers.UnreadableValue()
process_name = utility.array_to_string(proc.ImageFileName)
# if it is 0 then its a side effect of process ghosting
if proc.ImageFilePointer.vol.offset != 0:
try:
file_object = proc.ImageFilePointer
delete_pending = file_object.DeletePending
except exceptions.InvalidAddressException:
file_object = 0
# ImageFilePointer equal to 0 means process ghosting or similar techniques were used
else:
file_object = 0
if isinstance(delete_pending, int) and delete_pending not in [0, 1]:
vollog.debug(
f"Invalid delete_pending value {delete_pending} found for {process_name} {proc.UniqueProcessId}"
)
# delete_pending besides 0 or 1 = smear
if file_object == 0 or delete_pending == 1:
path = renderers.UnreadableValue()
if file_object:
with contextlib.suppress(exceptions.InvalidAddressException):
path = file_object.FileName.String
yield (
0,
(
proc.UniqueProcessId,
process_name,
format_hints.Hex(file_object),
delete_pending,
path,
),
)
def run(self):
filter_func = pslist.PsList.create_active_process_filter()
kernel = self.context.modules[self.config["kernel"]]
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("FILE_OBJECT", format_hints.Hex),
("DeletePending", str),
("Path", str),
],
self._generator(
pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
),
)
@@ -136,6 +136,29 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
filter_func = lambda x: x.UniqueProcessId not in filter_list
return filter_func
@classmethod
def create_active_process_filter(
cls,
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
"""A factory for producing a filter function that only returns
active, userland processes. This prevents plugins from operating on terminated
processes that are still in the process list due to smear or handle leaks as well
as kernel processes (System, Registry, etc.). Use of this filter for plugins searching
for system state anomalies significantly reduces false positive in smeared and terminated
processes.
Returns:
Filter function for passing to the `list_processes` method
"""
return lambda x: not (
x.is_valid()
and x.ActiveThreads > 0
and x.UniqueProcessId != 4
and x.InheritedFromUniqueProcessId != 4
and x.ExitTime.QuadPart == 0
and x.get_handle_count() != renderers.UnreadableValue()
)
@classmethod
def create_name_filter(
cls, name_list: List[str] = None, exclude: bool = False
+20 -12
View File
@@ -266,20 +266,28 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
if proc.vol.layer_name == kernel.layer_name:
vproc = proc
else:
vproc = self.virtual_process_from_physical(
self.context, kernel.layer_name, kernel.symbol_table_name, proc
try:
vproc = self.virtual_process_from_physical(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
proc,
)
except exceptions.PagedInvalidAddressException:
vproc = None
file_output = "Error outputting file"
if vproc:
file_handle = pslist.PsList.process_dump(
self.context,
kernel.symbol_table_name,
pe_table_name,
vproc,
self.open,
)
file_handle = pslist.PsList.process_dump(
self.context,
kernel.symbol_table_name,
pe_table_name,
vproc,
self.open,
)
file_output = "Error outputting file"
if file_handle:
file_output = file_handle.preferred_filename
if file_handle:
file_output = file_handle.preferred_filename
if not self.config["physical"]:
offset = proc.vol.offset
@@ -0,0 +1,251 @@
import datetime, logging, string
from volatility3.framework import constants, exceptions
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid
from volatility3.plugins.windows import (
handles,
info,
pslist,
psscan,
sessions,
thrdscan,
)
vollog = logging.getLogger(__name__)
class PsXView(plugins.PluginInterface):
"""Lists all processes found via four of the methods described in \"The Art of Memory Forensics,\" which may help
identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this
plugin's output in a terminal."""
# I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality
# which the original plugin used to do it.
# The sessions method is omitted because it begins with the list of processes found by Pslist anyway.
# Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the
# code I do have from it, and will happily share it if anyone else wants to add it.
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
valid_proc_name_chars = set(
string.ascii_lowercase + string.ascii_uppercase + "." + " "
)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="info", component=info.Info, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="psscan", component=psscan.PsScan, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="handles", component=handles.Handles, version=(1, 0, 0)
),
requirements.BooleanRequirement(
name="physical-offsets",
description="List processes with physical offsets instead of virtual offsets.",
optional=True,
),
]
def _proc_name_to_string(self, proc):
return proc.ImageFileName.cast(
"string", max_length=proc.ImageFileName.vol.count, errors="replace"
)
def _is_valid_proc_name(self, str):
for c in str:
if not c in self.valid_proc_name_chars:
return False
return True
def _filter_garbage_procs(self, proc_list):
return [
p
for p in proc_list
if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p))
]
def _translate_offset(self, offset):
if not self.config["physical-offsets"]:
return offset
kernel = self.context.modules[self.config["kernel"]]
layer_name = kernel.layer_name
try:
_original_offset, _original_length, offset, _length, _layer_name = list(
self.context.layers[layer_name].mapping(offset=offset, length=0)
)[0]
except exceptions.PagedInvalidAddressException:
vollog.debug(f"Page fault: unable to translate {offset:0x}")
return offset
def _proc_list_to_dict(self, tasks):
tasks = self._filter_garbage_procs(tasks)
return {self._translate_offset(proc.vol.offset): proc for proc in tasks}
def _check_pslist(self, tasks):
return self._proc_list_to_dict(tasks)
def _check_psscan(self, layer_name, symbol_table):
res = psscan.PsScan.scan_processes(
context=self.context, layer_name=layer_name, symbol_table=symbol_table
)
return self._proc_list_to_dict(res)
def _check_thrdscan(self):
ret = []
for ethread in thrdscan.ThrdScan.scan_threads(
self.context, module_name="kernel"
):
process = None
try:
process = ethread.owning_process()
if not process.is_valid():
continue
ret.append(process)
except AttributeError:
vollog.log(
constants.LOGLEVEL_VVV,
"Unable to find the owning process of ethread",
)
return self._proc_list_to_dict(ret)
def _check_csrss_handles(self, tasks, layer_name, symbol_table):
ret = []
for p in tasks:
name = self._proc_name_to_string(p)
if name == "csrss.exe":
try:
if p.has_member("ObjectTable"):
handles_plugin = handles.Handles(
context=self.context, config_path=self.config_path
)
hndls = list(handles_plugin.handles(p.ObjectTable))
for h in hndls:
if (
h.get_object_type(
handles_plugin.get_type_map(
self.context, layer_name, symbol_table
)
)
== "Process"
):
ret.append(h.Body.cast("_EPROCESS"))
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVV, "Cannot access eprocess object table"
)
return self._proc_list_to_dict(ret)
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
layer_name = kernel.layer_name
symbol_table = kernel.symbol_table_name
kdbg_list_processes = list(
pslist.PsList.list_processes(
context=self.context, layer_name=layer_name, symbol_table=symbol_table
)
)
# get processes from each source
processes = {}
processes["pslist"] = self._check_pslist(kdbg_list_processes)
processes["psscan"] = self._check_psscan(layer_name, symbol_table)
processes["thrdscan"] = self._check_thrdscan()
processes["csrss"] = self._check_csrss_handles(
kdbg_list_processes, layer_name, symbol_table
)
# print results
# list of lists of offsets
offsets = [list(processes[source].keys()) for source in processes]
# flatten to one list
offsets = sum(offsets, [])
# remove duplicates
offsets = set(offsets)
for offset in offsets:
proc = None
in_sources = {src: False for src in processes}
for source in processes:
if offset in processes[source]:
in_sources[source] = True
if not proc:
proc = processes[source][offset]
pid = proc.UniqueProcessId
name = self._proc_name_to_string(proc)
exit_time = proc.get_exit_time()
if type(exit_time) != datetime.datetime:
exit_time = ""
else:
exit_time = str(exit_time)
yield (
0,
(
format_hints.Hex(offset),
name,
pid,
in_sources["pslist"],
in_sources["psscan"],
in_sources["thrdscan"],
in_sources["csrss"],
exit_time,
),
)
def run(self):
offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)"
offset_str = "Offset" + offset_type
return TreeGrid(
[
(offset_str, format_hints.Hex),
("Name", str),
("PID", int),
("pslist", bool),
("psscan", bool),
("thrdscan", bool),
("csrss", bool),
("Exit Time", str),
],
self._generator(),
)
@@ -0,0 +1,98 @@
# 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
#
import logging
from typing import List
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import ssdt
from volatility3.plugins.windows.registry import hivelist
vollog = logging.getLogger(__name__)
class GetCellRoutine(interfaces.plugins.PluginInterface):
"""Reports registry hives with a hooked GetCellRoutine handler"""
_required_framework_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
]
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
collection = ssdt.SSDT.build_module_collection(
self.context, kernel.layer_name, kernel.symbol_table_name
)
# walk each hive and validate that the GetCellRoutine handler
# is inside of the kernel (ntoskrnl)
for hive_object in hivelist.HiveList.list_hives(
context=self.context,
base_config_path=self.config_path,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
):
hive = hive_object.hive
try:
cellroutine = hive.GetCellRoutine
except exceptions.InvalidAddressException:
continue
module_symbols = list(
collection.get_module_symbols_by_absolute_location(cellroutine)
)
if module_symbols:
for module_name, _ in module_symbols:
# GetCellRoutine handlers should only be in the kernel
if module_name not in constants.windows.KERNEL_MODULE_NAMES:
yield (
0,
(
format_hints.Hex(hive.vol.offset),
hive_object.get_name() or "",
module_name,
format_hints.Hex(cellroutine),
),
)
# Doesn't map to any module...
else:
yield (
0,
(
format_hints.Hex(hive.vol.offset),
hive_object.get_name() or "",
renderers.NotAvailableValue(),
format_hints.Hex(cellroutine),
),
)
def run(self):
return renderers.TreeGrid(
[
("Hive Offset", renderers.format_hints.Hex),
("Hive Name", str),
("GetCellRoutine Module", str),
("GetCellRoutine Handler", renderers.format_hints.Hex),
],
self._generator(),
)
@@ -17,11 +17,12 @@ from volatility3.framework.layers.registry import RegistryHive
from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols import intermed
from volatility3.plugins.windows.registry import hivelist
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
class UserAssist(interfaces.plugins.PluginInterface):
class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Print userassist registry keys and information."""
_required_framework_version = (2, 0, 0)
@@ -285,6 +286,10 @@ class UserAssist(interfaces.plugins.PluginInterface):
hive_offsets = [self.config.get("offset", None)]
kernel = self.context.modules[self.config["kernel"]]
self._reg_table_name = intermed.IntermediateSymbolTable.create(
self.context, self._config_path, "windows", "registry"
)
# get all the user hive offsets or use the one specified
for hive in hivelist.HiveList.list_hives(
context=self.context,
@@ -335,11 +340,17 @@ class UserAssist(interfaces.plugins.PluginInterface):
)
yield result
def run(self):
self._reg_table_name = intermed.IntermediateSymbolTable.create(
self.context, self._config_path, "windows", "registry"
)
def generate_timeline(self):
for row in self._generator():
_depth, row_data = row
# check the name and the timestamp to not be empty
if isinstance(row_data[5], str) and not isinstance(
row_data[10], renderers.NotApplicableValue
):
description = f"UserAssist: {row_data[5]} {row_data[2]} ({row_data[7]})"
yield (description, timeliner.TimeLinerType.MODIFIED, row_data[10])
def run(self):
return renderers.TreeGrid(
[
("Hive Offset", renderers.format_hints.Hex),
@@ -0,0 +1,610 @@
# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
import os
from datetime import datetime
from itertools import count
from typing import Iterator, List, Optional, Tuple
from volatility3.framework import constants, exceptions, interfaces, renderers, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.objects.utility import array_to_string
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import versions
from volatility3.framework.symbols.windows.extensions import pe, shimcache
from volatility3.plugins import timeliner
from volatility3.plugins.windows import modules, pslist, vadinfo
# from volatility3.plugins.windows import pslist, vadinfo, modules
vollog = logging.getLogger(__name__)
class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Reads Shimcache entries from the ahcache.sys AVL tree"""
_required_framework_version = (2, 0, 0)
# These checks must be completed from newest -> oldest OS version.
_win_version_file_map: List[Tuple[versions.OsDistinguisher, bool, str]] = [
(versions.is_win10, True, "shimcache-win10-x64"),
(versions.is_win10, False, "shimcache-win10-x86"),
(versions.is_windows_8_or_later, True, "shimcache-win8-x64"),
(versions.is_windows_8_or_later, False, "shimcache-win8-x86"),
(versions.is_windows_7, True, "shimcache-win7-x64"),
(versions.is_windows_7, False, "shimcache-win7-x86"),
(versions.is_vista_or_later, True, "shimcache-vista-x64"),
(versions.is_vista_or_later, False, "shimcache-vista-x86"),
(versions.is_2003, False, "shimcache-2003-x86"),
(versions.is_2003, True, "shimcache-2003-x64"),
(versions.is_windows_xp_sp3, False, "shimcache-xp-sp3-x86"),
(versions.is_windows_xp_sp2, False, "shimcache-xp-sp2-x86"),
(versions.is_xp_or_2003, True, "shimcache-xp-2003-x64"),
(versions.is_xp_or_2003, False, "shimcache-xp-2003-x86"),
]
NT_KRNL_MODS = ["ntoskrnl.exe", "ntkrnlpa.exe", "ntkrnlmp.exe", "ntkrpamp.exe"]
def generate_timeline(
self,
) -> 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
if isinstance(last_modified, datetime):
yield f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="modules", component=modules.Modules, version=(2, 0, 0)
),
]
@staticmethod
def create_shimcache_table(
context: interfaces.context.ContextInterface,
symbol_table: str,
config_path: str,
) -> str:
"""Creates a shimcache symbol table
Args:
context: The context to retrieve required elements (layers, symbol tables) from
symbol_table: The name of an existing symbol table containing the kernel symbols
config_path: The configuration path within the context of the symbol table to create
Returns:
The name of the constructed shimcache table
"""
native_types = context.symbol_space[symbol_table].natives
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
table_mapping = {"nt_symbols": symbol_table}
try:
symbol_filename = next(
filename
for version_check, for_64bit, filename in ShimcacheMem._win_version_file_map
if is_64bit == for_64bit
and version_check(context=context, symbol_table=symbol_table)
)
except StopIteration:
raise NotImplementedError("This version of Windows is not supported!")
vollog.debug(f"Using shimcache table {symbol_filename}")
return intermed.IntermediateSymbolTable.create(
context,
config_path,
os.path.join("windows", "shimcache"),
symbol_filename,
class_types=shimcache.class_types,
native_types=native_types,
table_mapping=table_mapping,
)
@classmethod
def find_shimcache_win_xp(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
kernel_symbol_table: str,
shimcache_symbol_table: str,
) -> Iterator[shimcache.SHIM_CACHE_ENTRY]:
"""Attempts to find the shimcache in a Windows XP memory image
:param context: The context to retrieve required elements (layers, symbol tables) from
:param layer_name: The name of the memory layer on which to operate.
:param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols
:param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols
"""
SHIM_NUM_ENTRIES_OFFSET = 0x8
SHIM_MAX_ENTRIES = 0x60 # 96 max entries in XP shim cache
SHIM_LRU_OFFSET = 0x10
SHIM_HEADER_SIZE = 0x190
SHIM_CACHE_ENTRY_SIZE = 0x228
seen = set()
for process in pslist.PsList.list_processes(
context, layer_name, kernel_symbol_table
):
pid = process.UniqueProcessId
vollog.debug("checking process %d" % pid)
for vad in vadinfo.VadInfo.list_vads(
process, lambda x: x.get_tag() == b"Vad " and x.Protection == 4
):
try:
proc_layer_name = process.add_process_layer()
proc_layer = context.layers[proc_layer_name]
except exceptions.InvalidAddressException:
continue
try:
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
except exceptions.InvalidAddressException:
continue
num_entries = context.object(
shimcache_symbol_table + constants.BANG + "unsigned int",
proc_layer_name,
vad.get_start() + SHIM_NUM_ENTRIES_OFFSET,
)
if num_entries > SHIM_MAX_ENTRIES:
continue
cache_idx_ptr = vad.get_start() + SHIM_LRU_OFFSET
for _ in range(num_entries):
cache_idx_val = proc_layer.context.object(
shimcache_symbol_table + constants.BANG + "unsigned long",
proc_layer_name,
cache_idx_ptr,
)
cache_idx_ptr += 4
if cache_idx_val > SHIM_MAX_ENTRIES - 1:
continue
shim_entry_offset = (
vad.get_start()
+ SHIM_HEADER_SIZE
+ (SHIM_CACHE_ENTRY_SIZE * cache_idx_val)
)
if not proc_layer.is_valid(shim_entry_offset):
continue
physical_addr = proc_layer.translate(shim_entry_offset)
if physical_addr in seen:
continue
seen.add(physical_addr)
shim_entry = proc_layer.context.object(
shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY",
proc_layer_name,
shim_entry_offset,
)
if not proc_layer.is_valid(shim_entry.vol.offset):
continue
if not shim_entry.is_valid():
continue
yield shim_entry
@classmethod
def find_shimcache_win_2k3_to_7(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
kernel_layer_name: str,
nt_symbol_table: str,
shimcache_symbol_table: str,
) -> Iterator[shimcache.SHIM_CACHE_ENTRY]:
"""Implements the algorithm to search for the shim cache on Windows 2000
(x64) through Windows 7 / 2008 R2. The algorithm consists of the following:
1) Find the NT kernel module's .data and PAGE sections
2) Iterate over every 4/8 bytes (depending on OS bitness) in the .data
section and test for the following:
a) offset represents a valid RTL_AVL_TABLE object
b) RTL_AVL_TABLE is preceeded by an ERESOURCE object
c) RTL_AVL_TABLE is followed by the beginning of the SHIM LRU list
:param context: The context to retrieve required elements (layers, symbol tables) from
:param layer_name: The name of the memory layer on which to operate.
:param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols
:param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols
"""
data_sec = cls.get_module_section_range(
context,
config_path,
kernel_layer_name,
nt_symbol_table,
cls.NT_KRNL_MODS,
".data",
)
mod_page = cls.get_module_section_range(
context,
config_path,
kernel_layer_name,
nt_symbol_table,
cls.NT_KRNL_MODS,
"PAGE",
)
# We require both in order to accurately handle AVL table
if not (data_sec and mod_page):
return None
data_sec_offset, data_sec_size = data_sec
mod_page_offset, mod_page_size = mod_page
addr_size = 8 if symbols.symbol_table_is_64bit(context, nt_symbol_table) else 4
shim_head = None
for offset in range(
data_sec_offset, data_sec_offset + data_sec_size, addr_size
):
shim_head = cls.try_get_shim_head_at_offset(
context,
shimcache_symbol_table,
nt_symbol_table,
kernel_layer_name,
mod_page_offset,
mod_page_offset + mod_page_size,
offset,
)
if shim_head:
break
if not shim_head:
return
for shim_entry in shim_head.ListEntry.to_list(
shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", "ListEntry"
):
yield shim_entry
@classmethod
def try_get_shim_head_at_offset(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
kernel_symbol_table: str,
layer_name: str,
mod_page_start: int,
mod_page_end: int,
offset: int,
) -> Optional[shimcache.SHIM_CACHE_ENTRY]:
"""Attempts to construct a SHIM_CACHE_HEAD within a layer of the given context,
using the provided offset within that layer, as well as the start and end offsets
of the kernel module's `PAGE` section start and end offsets.
If a number of validity checks are passed, this method will return the `SHIM_CACHE_HEAD`
object. Otherwise, `None` is returned.
"""
# print("checking RTL_AVL_TABLE at offset %s" % hex(offset))
rtl_avl_table = context.object(
symbol_table + constants.BANG + "_RTL_AVL_TABLE", layer_name, offset
)
if not rtl_avl_table.is_valid(mod_page_start, mod_page_end):
return None
vollog.debug(f"Candidate RTL_AVL_TABLE found at offset {hex(offset)}")
ersrc_size = context.symbol_space.get_type(
kernel_symbol_table + constants.BANG + "_ERESOURCE"
).size
ersrc_alignment = (
0x20
if symbols.symbol_table_is_64bit(context, kernel_symbol_table)
else 0x10
# 0x20 if context.symbol_space.get_type("pointer").size == 8 else 0x10
)
vollog.debug(
f"ERESOURCE size: {hex(ersrc_size)}, ERESOURCE alignment: {hex(ersrc_alignment)}"
)
eresource_rel_off = ersrc_size + ((offset - ersrc_size) % ersrc_alignment)
eresource_offset = offset - eresource_rel_off
vollog.debug("Constructing ERESOURCE at %s" % hex(eresource_offset))
eresource = context.object(
kernel_symbol_table + constants.BANG + "_ERESOURCE",
layer_name,
eresource_offset,
)
if not eresource.is_valid():
vollog.debug("ERESOURCE Invalid")
return None
shim_head_offset = offset + rtl_avl_table.vol.size
if not context.layers[layer_name].is_valid(shim_head_offset):
return None
shim_head = context.object(
symbol_table + constants.BANG + "SHIM_CACHE_ENTRY",
layer_name,
shim_head_offset,
)
if not shim_head.is_valid():
vollog.debug("shim head invalid")
return None
else:
vollog.debug("returning shim head")
return shim_head
@classmethod
def find_shimcache_win_8_or_later(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
kernel_layer_name: str,
nt_symbol_table: str,
shimcache_symbol_table: str,
) -> Iterator[shimcache.SHIM_CACHE_ENTRY]:
"""Attempts to locate and yield shimcache entries from a Windows 8 or later memory image.
:param context: The context to retrieve required elements (layers, symbol tables) from
:param layer_name: The name of the memory layer on which to operate.
:param kernel_symbol_table: The name of an existing symbol table containing the kernel symbols
:param shimcache_symbol_table: The name of a symbol table containing the hand-crafted shimcache symbols
"""
is_8_1_or_later = versions.is_windows_8_1_or_later(
context, nt_symbol_table
) or versions.is_win10(context, nt_symbol_table)
module_names = ["ahcache.sys"] if is_8_1_or_later else cls.NT_KRNL_MODS
vollog.debug(f"Searching for modules {module_names}")
data_sec = cls.get_module_section_range(
context,
config_path,
kernel_layer_name,
nt_symbol_table,
module_names,
".data",
)
mod_page = cls.get_module_section_range(
context,
config_path,
kernel_layer_name,
nt_symbol_table,
module_names,
"PAGE",
)
if not (data_sec and mod_page):
return None
mod_page_offset, mod_page_size = mod_page
data_sec_offset, data_sec_size = data_sec
# iterate over ahcache kernel module's .data section in search of *two* SHIM handles
shim_heads = []
vollog.debug(f"PAGE offset: {hex(mod_page_offset)}")
vollog.debug(f".data offset: {hex(data_sec_offset)}")
handle_type = context.symbol_space.get_type(
shimcache_symbol_table + constants.BANG + "SHIM_CACHE_HANDLE"
)
for offset in range(
data_sec_offset,
data_sec_offset + data_sec_size,
8 if symbols.symbol_table_is_64bit(context, nt_symbol_table) else 4,
):
vollog.debug(f"Building shim handle pointer at {hex(offset)}")
shim_handle = context.object(
object_type=shimcache_symbol_table + constants.BANG + "pointer",
layer_name=kernel_layer_name,
subtype=handle_type,
offset=offset,
)
if shim_handle.is_valid(mod_page_offset, mod_page_offset + mod_page_size):
if shim_handle.head is not None:
vollog.debug(
f"Found valid shim handle @ {hex(shim_handle.vol.offset)}"
)
shim_heads.append(shim_handle.head)
if len(shim_heads) == 2:
break
if len(shim_heads) != 2:
vollog.debug("Failed to identify two valid SHIM_CACHE_HANDLE structures")
return
# On Windows 8 x64, the frist cache contains the shim cache
# On Windows 8 x86, 8.1 x86/x64, and 10, the second cache contains the shim cache.
if (
not symbols.symbol_table_is_64bit(context, nt_symbol_table)
and not is_8_1_or_later
):
valid_head = shim_heads[1]
elif not is_8_1_or_later:
valid_head = shim_heads[0]
else:
valid_head = shim_heads[1]
for shim_entry in valid_head.ListEntry.to_list(
shimcache_symbol_table + constants.BANG + "SHIM_CACHE_ENTRY", "ListEntry"
):
if shim_entry.is_valid():
yield shim_entry
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
shimcache_table_name = self.create_shimcache_table(
self.context, kernel.symbol_table_name, self.config_path
)
c = count()
if versions.is_windows_8_or_later(self._context, kernel.symbol_table_name):
vollog.info("Finding shimcache entries for Windows 8.0+")
entries = self.find_shimcache_win_8_or_later(
self.context,
self.config_path,
kernel.layer_name,
kernel.symbol_table_name,
shimcache_table_name,
)
elif (
versions.is_2003(self.context, kernel.symbol_table_name)
or versions.is_vista_or_later(self.context, kernel.symbol_table_name)
or versions.is_windows_7(self.context, kernel.symbol_table_name)
):
vollog.info("Finding shimcache entries for Windows 2k3/Vista/7")
entries = self.find_shimcache_win_2k3_to_7(
self.context,
self.config_path,
kernel.layer_name,
kernel.symbol_table_name,
shimcache_table_name,
)
elif versions.is_windows_xp_sp2(
self._context, kernel.symbol_table_name
) or versions.is_windows_xp_sp3(self.context, kernel.symbol_table_name):
vollog.info("Finding shimcache entries for WinXP")
entries = self.find_shimcache_win_xp(
self._context,
kernel.layer_name,
kernel.symbol_table_name,
shimcache_table_name,
)
else:
vollog.warn("Cannot parse shimcache entries for this version of Windows")
return
for entry in entries:
try:
vollog.debug(f"SHIM_CACHE_ENTRY type: {entry.__class__}")
shim_entry = (
entry.last_modified,
entry.last_update,
entry.exec_flag,
(
format_hints.Hex(entry.file_size)
if isinstance(entry.file_size, int)
else entry.file_size
),
entry.file_path,
)
except exceptions.InvalidAddressException:
continue
yield (
0,
(next(c), *shim_entry),
)
def run(self):
return renderers.TreeGrid(
[
("Order", int),
("Last Modified", datetime),
("Last Update", datetime),
("Exec Flag", bool),
("File Size", format_hints.Hex),
("File Path", str),
],
self._generator(),
)
@classmethod
def get_module_section_range(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
layer_name: str,
symbol_table: str,
module_list: List[str],
section_name: str,
) -> Optional[Tuple[int, int]]:
"""Locates the size and offset of the first found module section
specified by name from the list of modules.
:param context: The context to operate on
:param layer_name: The memory layer to read from
:param module_list: A list of module names to search for the given section
:param section_name: The name of the section to search for.
:return: The offset and size of the module, if found; Otherwise, returns `None`
"""
try:
krnl_mod = next(
module
for module in modules.Modules.list_modules(
context, layer_name, symbol_table
)
if module.BaseDllName.String in module_list
)
except StopIteration:
return None
pe_table_name = intermed.IntermediateSymbolTable.create(
context,
interfaces.configuration.path_join(config_path, "pe"),
"windows",
"pe",
class_types=pe.class_types,
)
# code taken from Win32KBase._section_chunks (win32_core.py)
dos_header = context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
layer_name,
offset=krnl_mod.DllBase,
)
if not dos_header:
return None
nt_header = dos_header.get_nt_header()
try:
section = next(
sec
for sec in nt_header.get_sections()
if section_name.lower() == array_to_string(sec.Name).lower()
)
except StopIteration:
return None
section_offset = krnl_mod.DllBase + section.VirtualAddress
section_size = section.Misc.VirtualSize
return section_offset, section_size
@@ -30,7 +30,7 @@ class SSDT(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="modules", plugin=modules.Modules, version=(1, 0, 0)
name="modules", plugin=modules.Modules, version=(2, 0, 0)
),
]
@@ -0,0 +1,214 @@
# 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
#
import logging
from typing import List, Dict, Tuple, Generator
from volatility3.framework import renderers, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan
vollog = logging.getLogger(__name__)
class SupsiciousThreads(interfaces.plugins.PluginInterface):
"""Lists suspicious userland process threads"""
_required_framework_version = (2, 4, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
requirements.PluginRequirement(
name="threads", plugin=threads.Threads, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0)
),
]
def _get_ranges(
self,
kernel: interfaces.context.ModuleInterface,
all_ranges: Dict[int, List[Tuple[int, int, str, str]]],
proc,
) -> Tuple[int, int, str, str]:
"""
Maintains a hash table so each process' VADs
are only enumerated once per plugin run
"""
key = proc.vol.offset
if key not in all_ranges:
all_ranges[key] = []
for vad in proc.get_vad_root().traverse():
fn = vad.get_file_name()
if not isinstance(fn, str) or not fn:
fn = None
protection_string = vad.get_protection(
vadinfo.VadInfo.protect_values(
self.context, kernel.layer_name, kernel.symbol_table_name
),
vadinfo.winnt_protections,
)
all_ranges[key].append(
(vad.get_start(), vad.get_end(), protection_string, fn)
)
return all_ranges[key]
def _get_range(
self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int
) -> Tuple[int, str, str]:
"""
Walks a process' VADs looking for the one
containing `address`
Returns its base address, protection string, and mapped file, if any
"""
for start, end, protection_string, fn in ranges:
if start <= address < end:
return start, protection_string, fn
return None, None, None
def _check_thread_address(
self, exe_path: str, ranges, thread_address: int
) -> Generator[Tuple[str, str], None, None]:
vad_base, prot, vad_path = self._get_range(ranges, thread_address)
# threads outside of a VAD means either smear from this thread or this process' VAD tree
if vad_base is None:
return
if vad_path is None:
# set this so checks after report the non file backed region in the path column
vad_path = "<Non-File Backed Region>"
yield (
vad_path,
f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file",
)
# All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions
if prot != "PAGE_EXECUTE_WRITECOPY":
yield (
vad_path,
f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}",
)
# check for process hollowing type techniques that mapped in a second, malicious exe file
if (
exe_path
and vad_path.lower().endswith(".exe")
and (vad_path.lower() != exe_path.lower())
):
yield (
vad_path,
"VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process exectuable",
)
def _enumerate_processes(
self, kernel: interfaces.context.ModuleInterface, all_ranges
):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
for proc in pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
):
ranges = self._get_ranges(kernel, all_ranges, proc)
# smeared vads or process is terminating
if len(all_ranges[proc.vol.offset]) < 5:
continue
pid = proc.UniqueProcessId
proc_name = utility.array_to_string(proc.ImageFileName)
_, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress)
if not isinstance(exe_path, str):
exe_path = None
yield proc, pid, proc_name, exe_path, ranges
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
all_ranges = {}
for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes(
kernel, all_ranges
):
# processes often create multiple threads at the same address
# there is no benefit to checking the same address more than once per process
checked = set()
for thread in threads.Threads.list_threads(kernel, proc):
# do not process if a thread is exited or terminated (4 = Terminated)
if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4:
continue
# bail if accessing the threads members causes a page fault
info = thrdscan.ThrdScan.gather_thread_info(thread)
if not info:
continue
_, _, tid, start_address, _, _ = info
addresses = [
(start_address, "Start"),
(thread.Win32StartAddress, "Win32Start"),
]
for address, context in addresses:
if address in checked:
continue
checked.add(address)
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,
)
def run(self):
return renderers.TreeGrid(
[
("Process", str),
("PID", int),
("TID", int),
("Context", str),
("Address", format_hints.Hex),
("VAD Path", str),
("Note", str),
],
self._generator(),
)
@@ -0,0 +1,103 @@
# 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
#
# This module attempts to locate skeleton-key like function hooks.
# It does this by locating the CSystems array through a variety of methods,
# and then validating the entry for RC4 HMAC (0x17 / 23)
#
# For a thorough walkthrough on how the R&D was performed to develop this plugin,
# please see our blogpost here:
#
# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html
import logging
from volatility3.framework import symbols, interfaces
from volatility3.framework.configuration import requirements
from volatility3.plugins.windows import svclist, svcscan
from volatility3.framework.symbols.windows import versions
vollog = logging.getLogger(__name__)
class SvcDiff(svcscan.SvcScan):
"""Compares services found through list walking versus scanning to find rootkits"""
_required_framework_version = (2, 4, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enumeration_method = self.service_diff
@classmethod
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="svclist", component=svclist.SvcList, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="svcscan", component=svcscan.SvcScan, version=(3, 0, 0)
),
]
@classmethod
def service_diff(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
service_table_name: str,
service_binary_dll_map,
filter_func,
):
"""
On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list
and scan for services then report differences
"""
if not symbols.symbol_table_is_64bit(
context, symbol_table
) or not versions.is_win10_15063_or_later(
context=context, symbol_table=symbol_table
):
vollog.warning(
"This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples"
)
return
from_scan = set()
from_list = set()
records = {}
# collect unique service names from scanning
for service in svcscan.SvcScan.service_scan(
context,
layer_name,
symbol_table,
service_table_name,
service_binary_dll_map,
filter_func,
):
from_scan.add(service[6])
records[service[6]] = service
# collect services from listing walking
for service in svclist.SvcList.service_list(
context,
layer_name,
symbol_table,
service_table_name,
service_binary_dll_map,
filter_func,
):
from_list.add(service[6])
# report services found from scanning but not list walking
for hidden_service in from_scan - from_list:
yield records[hidden_service]
@@ -0,0 +1,115 @@
# 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 logging
from typing import List, Optional, Tuple
from volatility3.framework import interfaces, exceptions, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.symbols.windows import versions
from volatility3.plugins.windows import svcscan, pslist
from volatility3.framework.layers import scanners
vollog = logging.getLogger(__name__)
class SvcList(svcscan.SvcScan):
"""Lists services contained with the services.exe doubly linked list of services"""
_version = (1, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enumeration_method = self.service_list
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.PluginRequirement(
name="svcscan", plugin=svcscan.SvcScan, version=(3, 0, 0)
),
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def _get_exe_range(cls, proc) -> Optional[Tuple[int, int]]:
"""
Returns a tuple of starting,ending address for
the VAD containing services.exe
"""
vad_root = proc.get_vad_root()
for vad in vad_root.traverse():
filename = vad.get_file_name()
if isinstance(filename, str) and filename.lower().endswith(
"\\services.exe"
):
return [(vad.get_start(), vad.get_size())]
return None
@classmethod
def service_list(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
service_table_name: str,
service_binary_dll_map,
filter_func,
):
if not symbols.symbol_table_is_64bit(
context, symbol_table
) or not versions.is_win10_15063_or_later(
context=context, symbol_table=symbol_table
):
vollog.warning(
"This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples"
)
return
for proc in pslist.PsList.list_processes(
context=context,
layer_name=layer_name,
symbol_table=symbol_table,
filter_func=filter_func,
):
try:
layer_name = proc.add_process_layer()
except exceptions.InvalidAddressException:
vollog.warning(
"Unable to access memory of services.exe running with PID: {}".format(
proc.UniqueProcessId
)
)
continue
layer = context.layers[layer_name]
exe_range = cls._get_exe_range(proc)
if not exe_range:
vollog.warning(
"Could not find the application executable VAD for services.exe. Unable to proceed."
)
continue
for offset in layer.scan(
context=context,
scanner=scanners.BytesScanner(needle=b"Sc27"),
sections=exe_range,
):
for record in cls.enumerate_vista_or_later_header(
context,
service_table_name,
service_binary_dll_map,
layer_name,
offset,
):
yield record
+113 -58
View File
@@ -19,7 +19,7 @@ from volatility3.framework.layers import scanners
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import versions
from volatility3.framework.symbols.windows.extensions import services
from volatility3.framework.symbols.windows.extensions import services as services_types
from volatility3.plugins.windows import poolscanner, pslist, vadyarascan
from volatility3.plugins.windows.registry import hivelist
@@ -39,7 +39,11 @@ class SvcScan(interfaces.plugins.PluginInterface):
"""Scans for windows services."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (3, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._enumeration_method = self.service_scan
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -106,7 +110,7 @@ class SvcScan(interfaces.plugins.PluginInterface):
]
@staticmethod
def create_service_table(
def _create_service_table(
context: interfaces.context.ContextInterface,
symbol_table: str,
config_path: str,
@@ -140,18 +144,21 @@ class SvcScan(interfaces.plugins.PluginInterface):
config_path,
os.path.join("windows", "services"),
symbol_filename,
class_types=services.class_types,
class_types=services_types.class_types,
native_types=native_types,
)
def _get_service_key(self, kernel) -> Optional[objects.StructType]:
@staticmethod
def _get_service_key(
context, config_path: str, layer_name: str, symbol_table: str
) -> Optional[objects.StructType]:
for hive in hivelist.HiveList.list_hives(
context=self.context,
context=context,
base_config_path=interfaces.configuration.path_join(
self.config_path, "hivelist"
config_path, "hivelist"
),
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
layer_name=layer_name,
symbol_table=symbol_table,
filter_string="machine\\system",
):
# Get ControlSet\Services.
@@ -232,30 +239,55 @@ class SvcScan(interfaces.plugins.PluginInterface):
for service_key in services
}
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
@classmethod
def enumerate_vista_or_later_header(
cls,
context,
service_table_name,
service_binary_dll_map,
proc_layer_name,
offset,
):
if offset % 8:
return
service_table_name = self.create_service_table(
self.context, kernel.symbol_table_name, self.config_path
service_header = context.object(
service_table_name + constants.BANG + "_SERVICE_HEADER",
offset=offset,
layer_name=proc_layer_name,
)
# Building the dictionary ahead of time is much better for performance
# vs looking up each service's DLL individually.
services_key = self._get_service_key(kernel)
service_binary_dll_map = (
self._get_service_binary_map(services_key)
if services_key is not None
else {}
)
if not service_header.is_valid():
return
relative_tag_offset = self.context.symbol_space.get_type(
# since we walk the s-list backwards, if we've seen
# an object, then we've also seen all objects that
# exist before it, thus we can break at that time.
for service_record in service_header.ServiceRecord.traverse():
service_info = service_binary_dll_map.get(
service_record.get_name(),
ServiceBinaryInfo(
renderers.UnreadableValue(), renderers.UnreadableValue()
),
)
yield cls.get_record_tuple(service_record, service_info)
@classmethod
def service_scan(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
service_table_name: str,
service_binary_dll_map,
filter_func,
):
relative_tag_offset = context.symbol_space.get_type(
service_table_name + constants.BANG + "_SERVICE_RECORD"
).relative_child_offset("Tag")
filter_func = pslist.PsList.create_name_filter(["services.exe"])
is_vista_or_later = versions.is_vista_or_later(
context=self.context, symbol_table=kernel.symbol_table_name
context=context, symbol_table=symbol_table
)
if is_vista_or_later:
@@ -266,9 +298,9 @@ class SvcScan(interfaces.plugins.PluginInterface):
seen = []
for task in pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
context=context,
layer_name=layer_name,
symbol_table=symbol_table,
filter_func=filter_func,
):
proc_id = "Unknown"
@@ -283,15 +315,15 @@ class SvcScan(interfaces.plugins.PluginInterface):
)
continue
layer = self.context.layers[proc_layer_name]
layer = context.layers[proc_layer_name]
for offset in layer.scan(
context=self.context,
context=context,
scanner=scanners.BytesScanner(needle=service_tag),
sections=vadyarascan.VadYaraScan.get_vad_maps(task),
):
if not is_vista_or_later:
service_record = self.context.object(
service_record = context.object(
service_table_name + constants.BANG + "_SERVICE_RECORD",
offset=offset - relative_tag_offset,
layer_name=proc_layer_name,
@@ -306,37 +338,60 @@ class SvcScan(interfaces.plugins.PluginInterface):
renderers.UnreadableValue(), renderers.UnreadableValue()
),
)
yield (
0,
self.get_record_tuple(service_record, service_info),
)
yield cls.get_record_tuple(service_record, service_info)
else:
service_header = self.context.object(
service_table_name + constants.BANG + "_SERVICE_HEADER",
offset=offset,
layer_name=proc_layer_name,
)
if not service_header.is_valid():
continue
# since we walk the s-list backwards, if we've seen
# an object, then we've also seen all objects that
# exist before it, thus we can break at that time.
for service_record in service_header.ServiceRecord.traverse():
for service_record in cls.enumerate_vista_or_later_header(
context,
service_table_name,
service_binary_dll_map,
proc_layer_name,
offset,
):
if service_record in seen:
break
seen.append(service_record)
service_info = service_binary_dll_map.get(
service_record.get_name(),
ServiceBinaryInfo(
renderers.UnreadableValue(), renderers.UnreadableValue()
),
)
yield (
0,
self.get_record_tuple(service_record, service_info),
)
yield service_record
@classmethod
def get_prereq_info(cls, context, config_path, layer_name: str, symbol_table: str):
"""
Data structures and information needed to analyze service information
"""
service_table_name = cls._create_service_table(
context, symbol_table, config_path
)
services_key = cls._get_service_key(
context, config_path, layer_name, symbol_table
)
service_binary_dll_map = (
cls._get_service_binary_map(services_key)
if services_key is not None
else {}
)
filter_func = pslist.PsList.create_name_filter(["services.exe"])
return service_table_name, service_binary_dll_map, filter_func
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
service_table_name, service_binary_dll_map, filter_func = self.get_prereq_info(
self.context, self.config_path, kernel.layer_name, kernel.symbol_table_name
)
for record in self._enumeration_method(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
service_table_name,
service_binary_dll_map,
filter_func,
):
yield (0, record)
def run(self):
return renderers.TreeGrid(
@@ -3,7 +3,7 @@
##
import logging
import datetime
from typing import Iterable
from typing import Callable, Iterable
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
@@ -19,7 +19,11 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
# version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags
_required_framework_version = (2, 6, 0)
_version = (1, 0, 0)
_version = (1, 1, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.implementation = self.scan_threads
@classmethod
def get_requirements(cls):
@@ -38,20 +42,22 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
def scan_threads(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
module_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Scans for threads using the poolscanner module and constraints.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
module_name: Name of the module to use for scanning
Returns:
A list of _ETHREAD objects found by scanning memory for the "Thre" / "Thr\\xE5" pool signatures
"""
module = context.modules[module_name]
layer_name = module.layer_name
symbol_table = module.symbol_table_name
constraints = poolscanner.PoolScanner.builtin_constraints(
symbol_table, [b"Thr\xe5", b"Thre"]
)
@@ -62,45 +68,45 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
_constraint, mem_object, _header = result
yield mem_object
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
@classmethod
def gather_thread_info(cls, ethread):
try:
thread_offset = ethread.vol.offset
owner_proc_pid = ethread.Cid.UniqueProcess
thread_tid = ethread.Cid.UniqueThread
thread_start_addr = ethread.StartAddress
thread_create_time = (
ethread.get_create_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_exit_time = (
ethread.get_exit_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
except exceptions.InvalidAddressException:
vollog.debug("Thread invalid address {:#x}".format(ethread.vol.offset))
return None
for ethread in self.scan_threads(
self.context, kernel.layer_name, kernel.symbol_table_name
):
try:
thread_offset = ethread.vol.offset
owner_proc_pid = ethread.Cid.UniqueProcess
thread_tid = ethread.Cid.UniqueThread
thread_start_addr = ethread.StartAddress
thread_create_time = (
ethread.get_create_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_exit_time = (
ethread.get_exit_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
except (ValueError, exceptions.InvalidAddressException):
vollog.debug(
"Thread :{}, invalid address {} in layer {}".format(
thread_tid, thread_start_addr, kernel.layer_name
)
)
continue
return (
format_hints.Hex(thread_offset),
owner_proc_pid,
thread_tid,
format_hints.Hex(thread_start_addr),
thread_create_time,
thread_exit_time,
)
yield (
0,
(
format_hints.Hex(thread_offset),
owner_proc_pid,
thread_tid,
format_hints.Hex(thread_start_addr),
thread_create_time,
thread_exit_time,
),
)
def _generator(self, filter_func: Callable):
kernel_name = self.config["kernel"]
for ethread in self.implementation(self.context, kernel_name):
info = self.gather_thread_info(ethread)
if info:
yield (0, info)
def generate_timeline(self):
for row in self._generator():
filt_func = self.filter_func(self.config)
for row in self._generator(filt_func):
_depth, row_data = row
row_dict = {}
(
@@ -127,7 +133,14 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
row_dict["ExitTime"],
)
@classmethod
def filter_func(cls, config: interfaces.configuration.HierarchicalDict) -> Callable:
"""Returns a function that can filter this plugin's implementation method based on the config"""
return lambda x: False
def run(self):
filt_func = self.filter_func(self.config)
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
@@ -137,5 +150,5 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
("CreateTime", datetime.datetime),
("ExitTime", datetime.datetime),
],
self._generator(),
self._generator(filt_func),
)
@@ -0,0 +1,86 @@
# 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 logging
from typing import Callable, Iterable, List, Generator
from volatility3.framework import interfaces, constants
from volatility3.framework.configuration import requirements
from volatility3.plugins.windows import pslist, thrdscan
vollog = logging.getLogger(__name__)
class Threads(thrdscan.ThrdScan):
"""Lists process threads"""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.implementation = self.list_process_threads
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
requirements.PluginRequirement(
name="thrdscan", plugin=thrdscan.ThrdScan, version=(1, 1, 0)
),
]
@classmethod
def list_threads(
cls, kernel, proc: interfaces.objects.ObjectInterface
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Threads of a specific process.
Args:
proc: _EPROCESS object from which to list the VADs
Returns:
A list of threads based on the process and filtered based on the filter function
"""
seen = set()
for thread in proc.ThreadListHead.to_list(
f"{kernel.symbol_table_name}{constants.BANG}_ETHREAD", "ThreadListEntry"
):
if thread.vol.offset in seen:
break
seen.add(thread.vol.offset)
yield thread
@classmethod
def list_process_threads(
cls,
context: interfaces.context.ContextInterface,
module_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Runs through all processes and lists threads for each process"""
module = context.modules[module_name]
layer_name = module.layer_name
symbol_table_name = module.symbol_table_name
filter_func = pslist.PsList.create_pid_filter(context.config.get("pid", None))
for proc in pslist.PsList.list_processes(
context=context,
layer_name=layer_name,
symbol_table=symbol_table_name,
filter_func=filter_func,
):
for thread in cls.list_threads(module, proc):
yield thread
@@ -0,0 +1,212 @@
# 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
#
import logging
from typing import Iterator, List, Tuple, Iterable
from volatility3.framework import (
renderers,
interfaces,
constants,
symbols,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols.windows import versions
from volatility3.plugins.windows import ssdt, kpcrs
vollog = logging.getLogger(__name__)
class Timers(interfaces.plugins.PluginInterface):
"""Print kernel timers and associated module DPCs"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="kpcrs", plugin=kpcrs.KPCRs, version=(1, 0, 0)
),
]
@classmethod
def list_timers(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
layer_name: str,
symbol_table: str,
) -> Iterable[Tuple[str, int, str]]:
"""Lists all kernel timers.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
kernel_module_name: The name of the kernel module on which to operate
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
Yields:
A _KTIMER entry
"""
kernel = context.modules[kernel_module_name]
if versions.is_windows_7(
context=context, symbol_table=symbol_table
) or versions.is_windows_8_or_later(context=context, symbol_table=symbol_table):
# Starting with Windows 7, there is no more KiTimerTableListHead. The list is
# at _KPCR.PrcbData.TimerTable.TimerEntries
# See http://pastebin.com/FiRsGW3f
for kpcr in kpcrs.KPCRs.list_kpcrs(
context, kernel_module_name, layer_name, symbol_table
):
if hasattr(kpcr.Prcb.TimerTable, "TableState"):
for timer_entries in kpcr.Prcb.TimerTable.TimerEntries:
for timer_entry in timer_entries:
for timer in timer_entry.Entry.to_list(
symbol_table + constants.BANG + "_KTIMER",
"TimerListEntry",
):
yield timer
else:
for timer_entries in kpcr.Prcb.TimerTable.TimerEntries:
for timer in timer_entries.Entry.to_list(
symbol_table + constants.BANG + "_KTIMER",
"TimerListEntry",
):
yield timer
elif versions.is_xp_or_2003(
context=context, symbol_table=symbol_table
) or versions.is_vista_or_later(context=context, symbol_table=symbol_table):
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
if is_64bit or versions.is_vista_or_later(
context=context, symbol_table=symbol_table
):
# On XP x64, Windows 2003 SP1-SP2, and Vista SP0-SP2, KiTimerTableListHead
# is an array of 512 _KTIMER_TABLE_ENTRY structs.
array_size = 512
else:
# On XP SP0-SP3 x86 and Windows 2003 SP0, KiTimerTableListHead
# is an array of 256 _LIST_ENTRY for _KTIMERs.
array_size = 256
timer_table_list_head = kernel.object(
object_type="array",
offset=kernel.get_symbol("KiTimerTableListHead").address,
subtype=kernel.get_type("_LIST_ENTRY"),
count=array_size,
)
for table in timer_table_list_head:
for timer in table.to_list(
symbol_table + constants.BANG + "_KTIMER",
"TimerListEntry",
):
yield timer
else:
raise NotImplementedError("This version of Windows is not supported!")
def _generator(self) -> Iterator[Tuple]:
kernel = self.context.modules[self.config["kernel"]]
layer_name = kernel.layer_name
symbol_table = kernel.symbol_table_name
collection = ssdt.SSDT.build_module_collection(
self.context, kernel.layer_name, kernel.symbol_table_name
)
for timer in self.list_timers(
self.context, self.config["kernel"], layer_name, symbol_table
):
if not timer.valid_type():
continue
try:
dpc = timer.get_dpc()
if dpc == 0:
continue
if dpc.DeferredRoutine == 0:
continue
deferred_routine = dpc.DeferredRoutine
except Exception as e:
continue
module_symbols = list(
collection.get_module_symbols_by_absolute_location(deferred_routine)
)
if module_symbols:
for module_name, symbol_generator in module_symbols:
symbols_found = False
# we might have multiple symbols pointing to the same location
for symbol in symbol_generator:
symbols_found = True
yield (
0,
(
format_hints.Hex(timer.vol.offset),
timer.get_due_time(),
timer.Period,
timer.get_signaled(),
format_hints.Hex(deferred_routine),
module_name,
symbol.split(constants.BANG)[1],
),
)
# no symbols, but we at least can report the module name
if not symbols_found:
yield (
0,
(
format_hints.Hex(timer.vol.offset),
timer.get_due_time(),
timer.Period,
timer.get_signaled(),
format_hints.Hex(deferred_routine),
module_name,
renderers.NotAvailableValue(),
),
)
else:
# no module was found at the absolute location
yield (
0,
(
format_hints.Hex(timer.vol.offset),
timer.get_due_time(),
timer.Period,
timer.get_signaled(),
format_hints.Hex(deferred_routine),
renderers.NotAvailableValue(),
renderers.NotAvailableValue(),
),
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("DueTime", str),
("Period(ms)", int),
("Signaled", str),
("Routine", format_hints.Hex),
("Module", str),
("Symbol", str),
],
self._generator(),
)
@@ -33,7 +33,7 @@ class Passphrase(interfaces.plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="modules", component=modules.Modules, version=(1, 1, 0)
name="modules", component=modules.Modules, version=(2, 0, 0)
),
requirements.IntRequirement(
name="min-length",
@@ -0,0 +1,208 @@
# 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
# Full details on the techniques used in these plugins to detect EDR-evading malware
# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation
# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf
import logging
from typing import Dict, Tuple, List, Generator
from volatility3.framework import interfaces, exceptions
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.plugins.windows import pslist, pe_symbols
vollog = logging.getLogger(__name__)
class unhooked_system_calls(interfaces.plugins.PluginInterface):
"""Looks for signs of Skeleton Key malware"""
_required_framework_version = (2, 4, 0)
system_calls = {
"ntdll.dll": {
pe_symbols.wanted_names_identifier: [
"NtCreateThread",
"NtProtectVirtualMemory",
"NtReadVirtualMemory",
"NtOpenProcess",
"NtWriteFile",
"NtQueryVirtualMemory",
"NtAllocateVirtualMemory",
"NtWorkerFactoryWorkerReady",
"NtAcceptConnectPort",
"NtAddDriverEntry",
"NtAdjustPrivilegesToken",
"NtAlpcCreatePort",
"NtClose",
"NtCreateFile",
"NtCreateMutant",
"NtOpenFile",
"NtOpenIoCompletion",
"NtOpenJobObject",
"NtOpenKey",
"NtOpenKeyEx",
"NtOpenThread",
"NtOpenThreadToken",
"NtOpenThreadTokenEx",
"NtWriteVirtualMemory",
"NtTraceEvent",
"NtTranslateFilePath",
"NtUmsThreadYield",
"NtUnloadDriver",
"NtUnloadKey",
"NtUnloadKey2",
"NtUnloadKeyEx",
"NtCreateKey",
"NtCreateSection",
"NtDeleteKey",
"NtDeleteValueKey",
"NtDuplicateObject",
"NtQueryValueKey",
"NtReplaceKey",
"NtRequestWaitReplyPort",
"NtRestoreKey",
"NtSetContextThread",
"NtSetSecurityObject",
"NtSetValueKey",
"NtSystemDebugControl",
"NtTerminateProcess",
]
}
}
# This data structure is used to track unique implementations of functions across processes
# The outer dictionary holds the module name (e.g., ntdll.dll)
# The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module
# The innermost dictionary holds the unique implementation (bytes) of a function across processes
# Each implementation is tracked along with the process(es) that host it
# For systems without malware, all functions should have the same implementation
# When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations
_code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]]
@classmethod
def get_requirements(cls) -> List:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="pe_symbols", plugin=pe_symbols.PESymbols, version=(1, 0, 0)
),
]
def _gather_code_bytes(
self,
kernel: interfaces.context.ModuleInterface,
found_symbols: pe_symbols.found_symbols_type,
) -> _code_bytes_type:
"""
Enumerates the desired DLLs and function implementations in each process
Groups based on unique implementations of each DLLs' functions
The purpose is to detect when a function has different implementations (code)
in different processes.
This very effectively detects code injection.
"""
code_bytes: unhooked_system_calls._code_bytes_type = {}
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
)
for proc in procs:
try:
proc_id = proc.UniqueProcessId
proc_name = utility.array_to_string(proc.ImageFileName)
proc_layer_name = proc.add_process_layer()
except exceptions.InvalidAddressException:
continue
for dll_name, functions in found_symbols.items():
for func_name, func_addr in functions:
try:
fbytes = self.context.layers[proc_layer_name].read(
func_addr, 0x20
)
except exceptions.InvalidAddressException:
continue
# see the definition of _code_bytes_type for details of this data structure
if dll_name not in code_bytes:
code_bytes[dll_name] = {}
if func_name not in code_bytes[dll_name]:
code_bytes[dll_name][func_name] = {}
if fbytes not in code_bytes[dll_name][func_name]:
code_bytes[dll_name][func_name][fbytes] = []
code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name))
return code_bytes
def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]:
kernel = self.context.modules[self.config["kernel"]]
found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols(
self.context,
self.config_path,
kernel.layer_name,
kernel.symbol_table_name,
unhooked_system_calls.system_calls,
)
# code_bytes[dll_name][func_name][func_bytes]
code_bytes = self._gather_code_bytes(kernel, found_symbols)
# walk the functions that were evaluated
for functions in code_bytes.values():
# cbb is the distinct groups of bytes (instructions)
# for this function across processes
for func_name, cbb in functions.items():
# the dict key here is the raw instructions, which is not helpful to look at
# the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions)
cb = list(cbb.values())
# if all processes map to the same implementation, then no malware is present
if len(cb) == 1:
yield 0, (func_name, "", len(cb[0]))
else:
# if there are differing implementations then it means
# that malware has overwritten system call(s) in infected processes
# max_idx and small_idx find which implementation of a system call has the least processes
# as all observed malware and open source projects only infected a few targets, leaving the
# rest with the original EDR hooks in place
max_idx = 0 if len(cb[0]) > len(cb[1]) else 1
small_idx = (~max_idx) & 1
ps = []
# gather processes on small_idx since these are the malware infected ones
for pid, pname in cb[small_idx]:
ps.append("{:d}:{}".format(pid, pname))
proc_names = ", ".join(ps)
yield 0, (func_name, proc_names, len(cb[max_idx]))
def run(self) -> renderers.TreeGrid:
return renderers.TreeGrid(
[
("Function", str),
("Distinct Implementations", str),
("Total Implementations", int),
],
self._generator(),
)
@@ -0,0 +1,160 @@
# 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
#
import logging
import datetime
from typing import List, Iterable
from volatility3.framework import constants
from volatility3.framework import interfaces, symbols
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import configuration
from volatility3.framework.renderers import format_hints, conversion
from volatility3.framework.symbols import intermed
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the unloaded kernel modules."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
]
@staticmethod
def create_unloadedmodules_table(
context: interfaces.context.ContextInterface,
symbol_table: str,
config_path: str,
) -> str:
"""Creates a symbol table for the unloaded modules.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
symbol_table: The name of an existing symbol table containing the kernel symbols
config_path: The configuration path within the context of the symbol table to create
Returns:
The name of the constructed unloaded modules table
"""
native_types = context.symbol_space[symbol_table].natives
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
table_mapping = {"nt_symbols": symbol_table}
if is_64bit:
symbol_filename = "unloadedmodules-x64"
else:
symbol_filename = "unloadedmodules-x86"
return intermed.IntermediateSymbolTable.create(
context,
configuration.path_join(config_path, "unloadedmodules"),
"windows",
symbol_filename,
native_types=native_types,
table_mapping=table_mapping,
)
@classmethod
def list_unloadedmodules(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
unloadedmodule_table_name: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Lists all the unloaded modules in the primary layer.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
Returns:
A list of Unloaded Modules as retrieved from MmUnloadedDrivers
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
unloadedmodules_offset = ntkrnlmp.get_symbol("MmUnloadedDrivers").address
unloadedmodules = ntkrnlmp.object(
object_type="pointer",
offset=unloadedmodules_offset,
subtype="array",
)
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
if is_64bit:
unloaded_count_type = "unsigned long long"
else:
unloaded_count_type = "unsigned long"
last_unloadedmodule_offset = ntkrnlmp.get_symbol("MmLastUnloadedDriver").address
unloaded_count = ntkrnlmp.object(
object_type=unloaded_count_type, offset=last_unloadedmodule_offset
)
unloadedmodules_array = context.object(
object_type=unloadedmodule_table_name
+ constants.BANG
+ "_UNLOADED_DRIVERS",
layer_name=layer_name,
offset=unloadedmodules,
)
unloadedmodules_array.UnloadedDrivers.count = unloaded_count
for mod in unloadedmodules_array.UnloadedDrivers:
yield mod
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
unloadedmodule_table_name = self.create_unloadedmodules_table(
self.context, kernel.symbol_table_name, self.config_path
)
for mod in self.list_unloadedmodules(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
unloadedmodule_table_name,
):
yield (
0,
(
mod.Name.String,
format_hints.Hex(mod.StartAddress),
format_hints.Hex(mod.EndAddress),
conversion.wintime_to_datetime(mod.CurrentTime),
),
)
def generate_timeline(self):
for row in self._generator():
_depth, row_data = row
description = f"Unloaded Module: {row_data[0]}"
yield (description, timeliner.TimeLinerType.CHANGED, row_data[3])
def run(self):
return renderers.TreeGrid(
[
("Name", str),
("StartAddress", format_hints.Hex),
("EndAddress", format_hints.Hex),
("Time", datetime.datetime),
],
self._generator(),
)
@@ -3,7 +3,7 @@
#
import logging
from typing import Callable, List, Generator, Iterable, Type, Optional
from typing import Callable, List, Generator, Iterable, Type, Optional, Tuple
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
@@ -196,11 +196,31 @@ class VadInfo(interfaces.plugins.PluginInterface):
return file_handle
def _generator(self, procs):
def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[
Tuple[
int,
Tuple[
int,
str,
format_hints.Hex,
format_hints.Hex,
format_hints.Hex,
str,
str,
int,
int,
format_hints.Hex,
str,
str,
],
],
None,
None,
]:
kernel = self.context.modules[self.config["kernel"]]
kernel_layer = self.context.layers[kernel.layer_name]
def passthrough(_: interfaces.objects.ObjectInterface) -> bool:
def passthrough(x: interfaces.objects.ObjectInterface) -> bool:
return False
filter_func = passthrough
@@ -250,7 +270,7 @@ class VadInfo(interfaces.plugins.PluginInterface):
),
)
def run(self):
def run(self) -> renderers.TreeGrid:
kernel = self.context.modules[self.config["kernel"]]
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
@@ -18,7 +18,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
"""Scans all the Virtual Address Descriptor memory maps using yara."""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 1)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -32,11 +32,8 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0)
name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -59,6 +56,8 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
sanity_check = 1024 * 1024 * 1024 # 1 GB
for task in pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
@@ -67,18 +66,49 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
):
layer_name = task.add_process_layer()
layer = self.context.layers[layer_name]
for offset, rule_name, name, value in layer.scan(
context=self.context,
scanner=yarascan.YaraScanner(rules=rules),
sections=self.get_vad_maps(task),
):
yield 0, (
format_hints.Hex(offset),
task.UniqueProcessId,
rule_name,
name,
value,
)
for start, size in self.get_vad_maps(task):
if size > sanity_check:
vollog.debug(
f"VAD at 0x{start:x} over sanity-check size, not scanning"
)
continue
data = layer.read(start, size, True)
if not yarascan.YaraScan._yara_x:
for match in rules.match(data=data):
if yarascan.YaraScan.yara_returns_instances():
for match_string in match.strings:
for instance in match_string.instances:
yield 0, (
format_hints.Hex(instance.offset + start),
task.UniqueProcessId,
match.rule,
match_string.identifier,
instance.matched_data,
)
else:
for offset, name, value in match.strings:
yield 0, (
format_hints.Hex(offset + start),
task.UniqueProcessId,
match.rule,
name,
value,
)
else:
for match in rules.scan(data).matching_rules:
for match_string in match.patterns:
for instance in match_string.matches:
yield 0, (
format_hints.Hex(instance.offset + start),
task.UniqueProcessId,
f"{match.namespace}.{match.identifier}",
match_string.identifier,
data[
instance.offset : instance.offset
+ instance.length
],
)
@staticmethod
def get_vad_maps(
@@ -91,7 +121,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
task: The EPROCESS object of which to traverse the vad tree
Returns:
An iterable of tuples containing start and end addresses for each descriptor
An iterable of tuples containing start and size for each descriptor
"""
vad_root = task.get_vad_root()
for vad in vad_root.traverse():
@@ -46,10 +46,7 @@ class VerInfo(interfaces.plugins.PluginInterface):
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="modules", plugin=modules.Modules, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="dlllist", component=dlllist.DllList, version=(2, 0, 0)
name="modules", plugin=modules.Modules, version=(2, 0, 0)
),
requirements.BooleanRequirement(
name="extensive",
@@ -78,7 +78,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
)
else:
raise exceptions.SymbolError(
None, module.name, "Required structures not found"
"SystemVaRegions", module.name, "Required structures not found"
)
elif module.has_symbol("MiSystemVaType"):
system_range_start = module.object(
@@ -99,7 +99,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
)
else:
raise exceptions.SymbolError(
None, module.name, "Required structures not found"
"MiVisibleState", module.name, "Required structures not found"
)
return result
+88 -43
View File
@@ -13,20 +13,31 @@ from volatility3.framework.renderers import format_hints
vollog = logging.getLogger(__name__)
try:
import yara
USE_YARA_X = False
try:
import yara_x
USE_YARA_X = True
if tuple([int(x) for x in yara.__version__.split(".")]) < (3, 8):
raise ImportError
except ImportError:
vollog.info(
"Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available"
)
raise
try:
import yara
if tuple(int(x) for x in yara.__version__.split(".")) < (3, 8):
raise ImportError
vollog.debug("Using yara-python module")
except ImportError:
vollog.info(
"Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available"
)
raise
class YaraScanner(interfaces.layers.ScannerInterface):
_version = (2, 0, 0)
_version = (2, 1, 0)
# yara.Rules isn't exposed, so we can't type this properly
def __init__(self, rules) -> None:
@@ -34,37 +45,69 @@ class YaraScanner(interfaces.layers.ScannerInterface):
if rules is None:
raise ValueError("No rules provided to YaraScanner")
self._rules = rules
self.st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < (
4,
3,
self.st_object = (
None
if USE_YARA_X
else not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3)
)
def __call__(
self, data: bytes, data_offset: int
) -> Iterable[Tuple[int, str, str, bytes]]:
for match in self._rules.match(data=data):
if self.st_object:
for match_string in match.strings:
for instance in match_string.instances:
if USE_YARA_X:
for match in self._rules.scan(data).matching_rules:
for match_string in match.patterns:
for instance in match_string.matches:
yield (
instance.offset + data_offset,
match.rule,
f"{match.namespace}.{match.identifier}",
match_string.identifier,
instance.matched_data,
data[instance.offset : instance.offset + instance.length],
)
else:
for offset, name, value in match.strings:
yield (offset + data_offset, match.rule, name, value)
else:
for match in self._rules.match(data=data):
if YaraScan.yara_returns_instances():
for match_string in match.strings:
for instance in match_string.instances:
yield (
instance.offset + data_offset,
match.rule,
match_string.identifier,
instance.matched_data,
)
else:
for offset, name, value in match.strings:
yield (offset + data_offset, match.rule, name, value)
@staticmethod
def get_rule(rule):
if USE_YARA_X:
return yara_x.compile(f"rule r1 {{strings: $a = {rule} condition: $a}}")
return yara.compile(
sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"}
)
@staticmethod
def from_compiled_file(filepath):
with resources.ResourceAccessor().open(filepath, "rb") as fp:
if USE_YARA_X:
return yara_x.Rules.deserialize_from(file=fp)
return yara.load(file=fp)
@staticmethod
def from_file(filepath):
with resources.ResourceAccessor().open(filepath, "rb") as fp:
if USE_YARA_X:
return yara_x.compile(fp.read().decode())
return yara.compile(file=fp)
class YaraScan(plugins.PluginInterface):
"""Scans kernel memory using yara rules (string or file)."""
_required_framework_version = (2, 0, 0)
_version = (1, 2, 0)
# TODO: When the major version is bumped, take the opportunity to rename the yara_rules config to yara_string
# or something that makes more sense
_version = (2, 0, 0)
_yara_x = USE_YARA_X
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -99,10 +142,14 @@ class YaraScan(plugins.PluginInterface):
optional=True,
),
requirements.StringRequirement(
name="yara_rules", description="Yara rules (as a string)", optional=True
name="yara_string",
description="Yara rules (as a string)",
optional=True,
),
requirements.URIRequirement(
name="yara_file", description="Yara rules (as a file)", optional=True
name="yara_file",
description="Yara rules (as a file)",
optional=True,
),
# This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code
# As such, there's a separate option to run compiled files, as happened with yara-3.9 and later
@@ -119,32 +166,30 @@ class YaraScan(plugins.PluginInterface):
),
]
@classmethod
def yara_returns_instances(cls) -> bool:
return not tuple(int(x) for x in yara.__version__.split(".")) < (4, 3)
@classmethod
def process_yara_options(cls, config: Dict[str, Any]):
rules = None
if config.get("yara_rules", None) is not None:
rule = config["yara_rules"]
if config.get("yara_string") is not None:
rule = config["yara_string"]
if rule[0] not in ["{", "/"]:
rule = f'"{rule}"'
if config.get("case", False):
rule += " nocase"
if config.get("wide", False):
rule += " wide ascii"
rules = yara.compile(
sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"}
)
elif config.get("yara_source", None) is not None:
rules = yara.compile(source=config["yara_source"])
elif config.get("yara_file", None) is not None:
rules = yara.compile(
file=resources.ResourceAccessor().open(config["yara_file"], "rb")
)
elif config.get("yara_compiled_file", None) is not None:
rules = yara.load(
file=resources.ResourceAccessor().open(
config["yara_compiled_file"], "rb"
)
rules = YaraScanner.get_rule(rule)
elif config.get("yara_file") is not None:
vollog.debug(f"Plain file: {config['yara_file']} - yara-x: {USE_YARA_X}")
rules = YaraScanner.from_file(config["yara_file"])
elif config.get("yara_compiled_file") is not None:
vollog.debug(
f"Compiled file: {config['yara_compiled_file']} - yara-x: {USE_YARA_X}"
)
rules = YaraScanner.from_compiled_file(config["yara_compiled_file"])
else:
vollog.error("No yara rules, nor yara rules file were specified")
return rules
+10 -5
View File
@@ -11,6 +11,7 @@ from typing import Union
from volatility3.framework import interfaces, renderers
# FIXME: Move wintime_to_datetime() and unixtime_to_datetime() out of renderers, possibly framework.objects.utility
def wintime_to_datetime(
wintime: int,
) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]:
@@ -19,9 +20,11 @@ def wintime_to_datetime(
return renderers.NotApplicableValue()
unix_time = unix_time - 11644473600
try:
return datetime.datetime.utcfromtimestamp(unix_time)
# Windows sometimes throws OSErrors rather than ValueErrors when it can't convert a value
except (ValueError, OSError):
return datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc)
# Windows sometimes throws OSErrors rather than ValueError/OverflowError when it can't convert a value
# Since Python 3.3, this should raise OverflowError instead of ValueError. However, it was observed
# that even in Python 3.7.17, ValueError is still being raised.
except (ValueError, OverflowError, OSError):
return renderers.UnparsableValue()
@@ -33,8 +36,10 @@ def unixtime_to_datetime(
)
if unixtime > 0:
with contextlib.suppress(ValueError):
ret = datetime.datetime.utcfromtimestamp(unixtime)
# Since Python 3.3, this should raise OverflowError instead of ValueError. However, it was observed
# that even in Python 3.7.17, ValueError is still being raised. OSError is also raised on Linux
with contextlib.suppress(ValueError, OverflowError, OSError):
ret = datetime.datetime.fromtimestamp(unixtime, datetime.timezone.utc)
return ret
@@ -10,6 +10,8 @@ Text renderers should attempt to honour all hints provided in this module where
"""
from typing import Type, Union
from volatility3.framework import interfaces
class Bin(int):
"""A class to indicate that the integer value should be represented as a
@@ -66,3 +68,17 @@ class MultiTypeData(bytes):
and self.split_nulls == other.split_nulls
and self.show_hex == other.show_hex
)
BinOrAbsent = lambda x: (
Bin(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x
)
HexOrAbsent = lambda x: (
Hex(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x
)
HexBytesOrAbsent = lambda x: (
HexBytes(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x
)
MultiTypeDataOrAbsent = lambda x: (
MultiTypeData(x) if not isinstance(x, interfaces.renderers.BaseAbsentValue) else x
)
+1 -1
View File
@@ -282,7 +282,7 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface):
urls = list(cls.file_symbol_url(sub_path, filename))
if not urls:
raise FileNotFoundError(
"No symbol files found at provided filename: {}", filename
f"No symbol files found at provided filename: {filename}",
)
table_name = context.symbol_space.free_table_name(filename)
table = cls(
+414 -10
View File
@@ -1,6 +1,9 @@
# 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 math
import contextlib
from abc import ABC, abstractmethod
from typing import Iterator, List, Tuple, Optional, Union
from volatility3 import framework
@@ -29,12 +32,22 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("files_struct", extensions.files_struct)
self.set_type_class("kobject", extensions.kobject)
self.set_type_class("cred", extensions.cred)
self.set_type_class("inode", extensions.inode)
self.set_type_class("idr", extensions.IDR)
self.set_type_class("address_space", extensions.address_space)
self.set_type_class("page", extensions.page)
# Might not exist in the current symbols
self.optional_set_type_class("module", extensions.module)
self.optional_set_type_class("bpf_prog", extensions.bpf_prog)
self.optional_set_type_class("bpf_prog_aux", extensions.bpf_prog_aux)
self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t)
# kernels >= 4.18
self.optional_set_type_class("timespec64", extensions.timespec64)
# kernels < 4.18. Reuses timespec64 obj extension, since both has the same members
self.optional_set_type_class("timespec", extensions.timespec64)
# Mount
self.set_type_class("vfsmount", extensions.vfsmount)
# Might not exist in older kernels or the current symbols
@@ -61,7 +74,7 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
class LinuxUtilities(interfaces.configuration.VersionableInterface):
"""Class with multiple useful linux functions."""
_version = (2, 1, 0)
_version = (2, 1, 1)
_required_framework_version = (2, 0, 0)
framework.require_interface_version(*_required_framework_version)
@@ -156,13 +169,30 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
Returns:
str: Sock pipe pathname relative to the task's root directory.
"""
# FIXME: This function must be moved to the 'dentry' object extension
# Also, the scope of this function went beyond the sock pipe path, so we need to rename this.
# Once https://github.com/volatilityfoundation/volatility3/pull/1263 is merged, replace the
# dentry inode getters
if not (filp and filp.is_readable()):
return f"<invalid file pointer> {filp:x}"
dentry = filp.get_dentry()
if not (dentry and dentry.is_readable()):
return f"<invalid dentry pointer> {dentry:x}"
kernel_module = cls.get_module_from_volobj_type(context, dentry)
sym_addr = dentry.d_op.d_dname
if not (sym_addr and sym_addr.is_readable()):
return f"<invalid d_dname pointer> {sym_addr:x}"
symbs = list(kernel_module.get_symbols_by_absolute_location(sym_addr))
inode = dentry.d_inode
if not (inode and inode.is_readable() and inode.is_valid()):
return f"<invalid dentry inode> {inode:x}"
if len(symbs) == 1:
sym = symbs[0].split(constants.BANG)[1]
@@ -178,15 +208,41 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
elif sym == "simple_dname":
pre_name = cls._get_path_file(task, filp)
elif sym == "ns_dname":
# From Kernels 3.19
# In Kernels >= 6.9, see Linux kernel commit 1fa08aece42512be072351f482096d5796edf7ca
# ns_common->stashed change from 'atomic64_t' to 'dentry*'
try:
ns_common_type = kernel_module.get_type("ns_common")
stashed_template = ns_common_type.child_template("stashed")
stashed_type_full_name = stashed_template.vol.type_name
stashed_type_name = stashed_type_full_name.split(constants.BANG)[1]
if stashed_type_name == "atomic64_t":
# 3.19 <= Kernels < 6.9
fsdata_ptr = dentry.d_fsdata
if not (fsdata_ptr and fsdata_ptr.is_readable()):
raise IndexError
ns_ops = fsdata_ptr.dereference().cast("proc_ns_operations")
else:
# Kernels >= 6.9
private_ptr = inode.i_private
if not (private_ptr and private_ptr.is_readable()):
raise IndexError
ns_common = private_ptr.dereference().cast("ns_common")
ns_ops = ns_common.ops
pre_name = utility.pointer_to_string(ns_ops.name, 255)
except IndexError:
pre_name = "<unsupported ns_dname implementation>"
else:
pre_name = f"<unsupported d_op symbol: {sym}>"
ret = f"{pre_name}:[{dentry.d_inode.i_ino:d}]"
pre_name = f"<unsupported d_op symbol> {sym}"
else:
ret = f"<invalid d_dname pointer> {sym_addr:x}"
pre_name = f"<unknown d_dname pointer> {sym_addr:x}"
return ret
return f"{pre_name}:[{inode.i_ino:d}]"
@classmethod
def path_for_file(cls, context, task, filp) -> str:
@@ -406,9 +462,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
Returns:
A kernel object (vmlinux)
"""
symbol_table_arr = volobj.vol.type_name.split("!", 1)
symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None
symbol_table = volobj.get_symbol_table_name()
module_names = context.modules.get_modules_by_symbol_tables(symbol_table)
module_names = list(module_names)
@@ -419,3 +473,353 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
kernel = context.modules[kernel_module_name]
return kernel
class IDStorage(ABC):
"""Abstraction to support both XArray and RadixTree"""
# Dynamic values, these will be initialized later
CHUNK_SHIFT = None
CHUNK_SIZE = None
CHUNK_MASK = None
def __init__(
self,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
):
self.vmlinux = context.modules[kernel_module_name]
self.vmlinux_layer = self.vmlinux.context.layers[self.vmlinux.layer_name]
self.pointer_size = self.vmlinux.get_type("pointer").size
# Dynamically work out the (XA_CHUNK|RADIX_TREE_MAP)_SHIFT values based on
# the node.slots[] array size
node_type = self.vmlinux.get_type(self.node_type_name)
slots_array_size = node_type.child_template("slots").count
# Calculate the LSB index - 1
self.CHUNK_SHIFT = slots_array_size.bit_length() - 1
self.CHUNK_SIZE = 1 << self.CHUNK_SHIFT
self.CHUNK_MASK = self.CHUNK_SIZE - 1
@classmethod
def choose_id_storage(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
) -> "IDStorage":
"""Returns the appropriate ID storage data structure instance for the current kernel implementation.
This is used by the IDR and the PageCache to choose between the XArray and RadixTree.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
kernel_module_name: The name of the kernel module on which to operate
Returns:
The appropriate ID storage instance for the current kernel
"""
vmlinux = context.modules[kernel_module_name]
address_space_type = vmlinux.get_type("address_space")
address_space_has_i_pages = address_space_type.has_member("i_pages")
i_pages_type_name = (
address_space_type.child_template("i_pages").vol.type_name
if address_space_has_i_pages
else ""
)
i_pages_is_xarray = i_pages_type_name.endswith(constants.BANG + "xarray")
i_pages_is_radix_tree_root = i_pages_type_name.endswith(
constants.BANG + "radix_tree_root"
) and vmlinux.get_type("radix_tree_root").has_member("xa_head")
if i_pages_is_xarray or i_pages_is_radix_tree_root:
return XArray(context, kernel_module_name)
else:
return RadixTree(context, kernel_module_name)
@property
@abstractmethod
def node_type_name(self) -> str:
"""Returns the Tree implementation node type name
Returns:
A string with the node type name
"""
raise NotImplementedError()
@property
def tag_internal_value(self) -> int:
"""Returns the internal node flag for the tree"""
raise NotImplementedError()
@abstractmethod
def node_is_internal(self, nodep) -> bool:
"""Checks if the node is internal"""
raise NotImplementedError
@abstractmethod
def is_node_tagged(self, nodep) -> bool:
"""Checks if the node pointer is tagged"""
raise NotImplementedError
@abstractmethod
def untag_node(self, nodep) -> int:
"""Untags a node pointer"""
raise NotImplementedError
@abstractmethod
def get_tree_height(self, treep) -> int:
"""Returns the tree height"""
raise NotImplementedError
@abstractmethod
def get_node_height(self, nodep) -> int:
"""Returns the node height"""
raise NotImplementedError
@abstractmethod
def get_head_node(self, tree) -> int:
"""Returns a pointer to the tree's head"""
raise NotImplementedError
@abstractmethod
def is_valid_node(self, nodep) -> bool:
"""Validates a node pointer"""
raise NotImplementedError
def nodep_to_node(self, nodep) -> interfaces.objects.ObjectInterface:
"""Instanciates a tree node from its pointer
Args:
nodep: Pointer to the XArray/RadixTree node
Returns:
A XArray/RadixTree node instance
"""
node = self.vmlinux.object(self.node_type_name, offset=nodep, absolute=True)
return node
def _slot_to_nodep(self, slot) -> int:
if self.node_is_internal(slot):
nodep = slot & ~self.tag_internal_value
else:
nodep = slot
return nodep
def _iter_node(self, nodep, height) -> int:
node = self.nodep_to_node(nodep)
node_slots = node.slots
for off in range(self.CHUNK_SIZE):
slot = node_slots[off]
if slot == 0:
continue
nodep = self._slot_to_nodep(slot)
if height == 1:
if self.is_valid_node(nodep):
yield nodep
else:
for child_node in self._iter_node(nodep, height - 1):
yield child_node
def get_entries(self, root: interfaces.objects.ObjectInterface) -> int:
"""Walks the tree data structure
Args:
root: The tree root object
Yields:
A tree node pointer
"""
height = self.get_tree_height(root.vol.offset)
nodep = self.get_head_node(root)
if not nodep:
return
# Keep the internal flag before untagging it
is_internal = self.node_is_internal(nodep)
if self.is_node_tagged(nodep):
nodep = self.untag_node(nodep)
if is_internal:
height = self.get_node_height(nodep)
if height == 0:
if self.is_valid_node(nodep):
yield nodep
else:
for child_node in self._iter_node(nodep, height):
yield child_node
class XArray(IDStorage):
XARRAY_TAG_MASK = 3
XARRAY_TAG_INTERNAL = 2
def get_tree_height(self, treep) -> int:
return 0
@property
def node_type_name(self) -> str:
return "xa_node"
@property
def tag_internal_value(self) -> int:
return self.XARRAY_TAG_INTERNAL
def get_node_height(self, nodep) -> int:
node = self.nodep_to_node(nodep)
return (node.shift / self.CHUNK_SHIFT) + 1
def get_head_node(self, tree) -> int:
return tree.xa_head
def node_is_internal(self, nodep) -> bool:
return (nodep & self.XARRAY_TAG_MASK) == self.XARRAY_TAG_INTERNAL
def is_node_tagged(self, nodep) -> bool:
return (nodep & self.XARRAY_TAG_MASK) != 0
def untag_node(self, nodep) -> int:
return nodep & (~self.XARRAY_TAG_MASK)
def is_valid_node(self, nodep) -> bool:
# It should have the tag mask clear
return not self.is_node_tagged(nodep)
class RadixTree(IDStorage):
RADIX_TREE_INTERNAL_NODE = 1
RADIX_TREE_EXCEPTIONAL_ENTRY = 2
RADIX_TREE_ENTRY_MASK = 3
# Dynamic values. These will be initialized later
RADIX_TREE_INDEX_BITS = None
RADIX_TREE_MAX_PATH = None
RADIX_TREE_HEIGHT_SHIFT = None
RADIX_TREE_HEIGHT_MASK = None
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
char_bits = 8
self.RADIX_TREE_INDEX_BITS = char_bits * self.pointer_size
self.RADIX_TREE_MAX_PATH = int(
math.ceil(self.RADIX_TREE_INDEX_BITS / float(self.CHUNK_SHIFT))
)
self.RADIX_TREE_HEIGHT_SHIFT = self.RADIX_TREE_MAX_PATH + 1
self.RADIX_TREE_HEIGHT_MASK = (1 << self.RADIX_TREE_HEIGHT_SHIFT) - 1
if not self.vmlinux.has_type("radix_tree_root"):
# In kernels 4.20, RADIX_TREE_INTERNAL_NODE flag took RADIX_TREE_EXCEPTIONAL_ENTRY's
# value. RADIX_TREE_EXCEPTIONAL_ENTRY was removed but that's managed in is_valid_node()
# Note that the Radix Tree is still in use for IDR, even after kernels 4.20 when XArray
# mostly replace it
self.RADIX_TREE_INTERNAL_NODE = 2
@property
def node_type_name(self) -> str:
return "radix_tree_node"
@property
def tag_internal_value(self) -> int:
return self.RADIX_TREE_INTERNAL_NODE
def get_tree_height(self, treep) -> int:
with contextlib.suppress(exceptions.SymbolError):
if self.vmlinux.get_type("radix_tree_root").has_member("height"):
# kernels < 4.7.10
radix_tree_root = self.vmlinux.object(
"radix_tree_root", offset=treep, absolute=True
)
return radix_tree_root.height
# kernels >= 4.7.10
return 0
def _radix_tree_maxindex(self, node, height) -> int:
"""Return the maximum key which can be store into a radix tree with this height."""
if not self.vmlinux.has_symbol("height_to_maxindex"):
# Kernels >= 4.7
return (self.CHUNK_SIZE << node.shift) - 1
else:
# Kernels < 4.7
height_to_maxindex_array = self.vmlinux.object_from_symbol(
"height_to_maxindex"
)
maxindex = height_to_maxindex_array[height]
return maxindex
def get_node_height(self, nodep) -> int:
node = self.nodep_to_node(nodep)
if hasattr(node, "shift"):
# 4.7 <= Kernels < 4.20
return (node.shift / self.CHUNK_SHIFT) + 1
elif hasattr(node, "path"):
# 3.15 <= Kernels < 4.7
return node.path & self.RADIX_TREE_HEIGHT_MASK
elif hasattr(node, "height"):
# Kernels < 3.15
return node.height
else:
raise exceptions.VolatilityException("Cannot find radix-tree node height")
def get_head_node(self, tree) -> int:
return tree.rnode
def node_is_internal(self, nodep) -> bool:
return (nodep & self.RADIX_TREE_INTERNAL_NODE) != 0
def is_node_tagged(self, nodep) -> bool:
return self.node_is_internal(nodep)
def untag_node(self, nodep) -> int:
return nodep & (~self.RADIX_TREE_ENTRY_MASK)
def is_valid_node(self, nodep) -> bool:
# In kernels 4.20, exceptional nodes were removed and internal entries took their bitmask
if self.vmlinux.has_type("radix_tree_root"):
return (
nodep & self.RADIX_TREE_ENTRY_MASK
) != self.RADIX_TREE_EXCEPTIONAL_ENTRY
return True
class PageCache(object):
"""Linux Page Cache abstraction"""
def __init__(
self,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
page_cache: interfaces.objects.ObjectInterface,
):
"""
Args:
context: interfaces.context.ContextInterface,
kernel_module_name: The name of the kernel module on which to operate
page_cache: Page cache address space
"""
self.vmlinux = context.modules[kernel_module_name]
self._page_cache = page_cache
self._idstorage = IDStorage.choose_id_storage(context, kernel_module_name)
def get_cached_pages(self) -> interfaces.objects.ObjectInterface:
"""Returns all page cache contents
Yields:
Page objects
"""
for page_addr in self._idstorage.get_entries(self._page_cache.i_pages):
if not page_addr:
continue
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
if page:
yield page
@@ -4,10 +4,16 @@
import collections.abc
import logging
import functools
import binascii
import stat
from datetime import datetime
import socket as socket_module
from typing import Generator, Iterable, Iterator, Optional, Tuple, List
from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
from volatility3.framework.renderers import conversion
from volatility3.framework.configuration import requirements
from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY
from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS
from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS
@@ -44,7 +50,7 @@ class module(generic.GenericIntelProcess):
).choices
except exceptions.SymbolError:
vollog.debug(
f"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4"
"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4"
)
# set to empty dict to show that the enum was not found, and so shouldn't be searched for again
self._mod_mem_type = {}
@@ -817,6 +823,26 @@ class dentry(objects.StructType):
current_dentry = current_dentry.d_parent
return None
def get_subdirs(self) -> interfaces.objects.ObjectInterface:
"""Walks dentry subdirs
Yields:
A dentry object
"""
if self.has_member("d_sib") and self.has_member("d_children"):
# kernels >= 6.8
walk_member = "d_sib"
list_head_member = self.d_children.first
elif self.has_member("d_child") and self.has_member("d_subdirs"):
# 2.5.0 <= kernels < 6.8
walk_member = "d_child"
list_head_member = self.d_subdirs
else:
raise exceptions.VolatilityException("Unsupported dentry type")
dentry_type_name = self.get_symbol_table_name() + constants.BANG + "dentry"
yield from list_head_member.to_list(dentry_type_name, walk_member)
class struct_file(objects.StructType):
def get_dentry(self) -> interfaces.objects.ObjectInterface:
@@ -933,7 +959,8 @@ class mount(objects.StructType):
MNT_RELATIME: "relatime",
}
def get_mnt_sb(self):
def get_mnt_sb(self) -> int:
"""Returns a pointer to the super_block"""
if self.has_member("mnt"):
return self.mnt.mnt_sb
elif self.has_member("mnt_sb"):
@@ -1248,6 +1275,7 @@ class vfsmount(objects.StructType):
return self._get_real_mnt().has_parent()
def get_mnt_sb(self):
"""Returns a pointer to the super_block"""
return self.mnt_sb
def get_flags_access(self) -> str:
@@ -1581,20 +1609,59 @@ class xdp_sock(objects.StructType):
class bpf_prog(objects.StructType):
def get_type(self):
def get_type(self) -> Union[str, None]:
"""Returns a string with the eBPF program type"""
# The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to
# 4.1.52 before it was moved to `bpf_prog::type`
if self.has_member("type"):
# kernel >= 4.1.52
return self.type
return self.type.description
if self.has_member("aux") and self.aux:
if self.aux.has_member("prog_type"):
# 3.18.140 <= kernel < 4.1.52
return self.aux.prog_type
return self.aux.prog_type.description
# kernel < 3.18.140
raise AttributeError("Unable to find the BPF type")
return None
def get_tag(self) -> Union[str, None]:
"""Returns a string with the eBPF program tag"""
# 'tag' was added in kernels 4.10
if not self.has_member("tag"):
return None
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
prog_tag_addr = self.tag.vol.offset
prog_tag_size = self.tag.count
prog_tag_bytes = vmlinux_layer.read(prog_tag_addr, prog_tag_size)
prog_tag = binascii.hexlify(prog_tag_bytes).decode()
return prog_tag
def get_name(self) -> Union[str, None]:
"""Returns a string with the eBPF program name"""
if not self.has_member("aux"):
# 'prog_aux' was added in kernels 3.18
return None
return self.aux.get_name()
class bpf_prog_aux(objects.StructType):
def get_name(self) -> Union[str, None]:
"""Returns a string with the eBPF program name"""
if not self.has_member("name"):
# 'name' was added in kernels 4.15
return None
if not self.name:
return None
return utility.array_to_string(self.name)
class cred(objects.StructType):
@@ -1761,3 +1828,383 @@ class kernel_cap_t(kernel_cap_struct):
)
return cap_value & self.get_kernel_cap_full()
class timespec64(objects.StructType):
def to_datetime(self) -> datetime:
"""Returns the respective aware datetime"""
dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9)
return dt
class inode(objects.StructType):
def is_valid(self) -> bool:
# i_count is a 'signed' counter (atomic_t). Smear, or essentially a wrong inode
# pointer, will easily cause an integer overflow here.
return self.i_ino > 0 and self.i_count.counter >= 0
@property
def is_dir(self) -> bool:
"""Returns True if the inode is a directory"""
return stat.S_ISDIR(self.i_mode) != 0
@property
def is_reg(self) -> bool:
"""Returns True if the inode is a regular file"""
return stat.S_ISREG(self.i_mode) != 0
@property
def is_link(self) -> bool:
"""Returns True if the inode is a symlink"""
return stat.S_ISLNK(self.i_mode) != 0
@property
def is_fifo(self) -> bool:
"""Returns True if the inode is a FIFO"""
return stat.S_ISFIFO(self.i_mode) != 0
@property
def is_sock(self) -> bool:
"""Returns True if the inode is a socket"""
return stat.S_ISSOCK(self.i_mode) != 0
@property
def is_block(self) -> bool:
"""Returns True if the inode is a block device"""
return stat.S_ISBLK(self.i_mode) != 0
@property
def is_char(self) -> bool:
"""Returns True if the inode is a char device"""
return stat.S_ISCHR(self.i_mode) != 0
@property
def is_sticky(self) -> bool:
"""Returns True if the sticky bit is set"""
return (self.i_mode & stat.S_ISVTX) != 0
def get_inode_type(self) -> Union[str, None]:
"""Returns inode type name
Returns:
The inode type name
"""
if self.is_dir:
return "DIR"
elif self.is_reg:
return "REG"
elif self.is_link:
return "LNK"
elif self.is_fifo:
return "FIFO"
elif self.is_sock:
return "SOCK"
elif self.is_char:
return "CHR"
elif self.is_block:
return "BLK"
else:
return None
def _time_member_to_datetime(self, member) -> datetime:
if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"):
# kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32
# Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853
return conversion.unixtime_to_datetime(
self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9
)
elif self.has_member(f"__{member}"):
# 6.6 <= kernels < 6.11 it's a timespec64
# Ref Linux commit 13bc24457850583a2e7203ded05b7209ab4bc5ef / 12cd44023651666bd44baa36a5c999698890debb
return self.member(f"__{member}").to_datetime()
elif self.has_member(member):
# In kernels < 6.6 it's a timespec64 or timespec
return self.member(member).to_datetime()
else:
raise exceptions.VolatilityException(
"Unsupported kernel inode type implementation"
)
def get_access_time(self) -> datetime:
"""Returns the inode's last access time
This is updated when inode contents are read
Returns:
A datetime with the inode's last access time
"""
return self._time_member_to_datetime("i_atime")
def get_modification_time(self) -> datetime:
"""Returns the inode's last modification time
This is updated when the inode contents change
Returns:
A datetime with the inode's last data modification time
"""
return self._time_member_to_datetime("i_mtime")
def get_change_time(self) -> datetime:
"""Returns the inode's last change time
This is updated when the inode metadata changes
Returns:
A datetime with the inode's last change time
"""
return self._time_member_to_datetime("i_ctime")
def get_file_mode(self) -> str:
"""Returns the inode's file mode as string of the form '-rwxrwxrwx'.
Returns:
The inode's file mode string
"""
return stat.filemode(self.i_mode)
def get_pages(self) -> interfaces.objects.ObjectInterface:
"""Gets the inode's cached pages
Yields:
The inode's cached pages
"""
if not self.i_size:
return
elif not (self.i_mapping and self.i_mapping.nrpages > 0):
return
page_cache = linux.PageCache(
context=self._context,
kernel_module_name="kernel",
page_cache=self.i_mapping.dereference(),
)
yield from page_cache.get_cached_pages()
def get_contents(self):
"""Get the inode cached pages from the page cache
Yields:
page_index (int): The page index in the Tree. File offset is page_index * PAGE_SIZE.
page_content (str): The page content
"""
for page_obj in self.get_pages():
page_index = int(page_obj.index)
page_content = page_obj.get_content()
yield page_index, page_content
class address_space(objects.StructType):
@property
def i_pages(self):
"""Returns the appropriate member containing the page cache tree"""
if self.has_member("i_pages"):
# Kernel >= 4.17
return self.member("i_pages")
elif self.has_member("page_tree"):
# Kernel < 4.17
return self.member("page_tree")
raise exceptions.VolatilityException("Unsupported page cache tree")
class page(objects.StructType):
@property
@functools.lru_cache()
def pageflags_enum(self) -> Dict:
"""Returns 'pageflags' enumeration key/values
Returns:
A dictionary with the pageflags enumeration key/values
"""
# FIXME: It would be even better to use @functools.cached_property instead,
# however, this requires Python +3.8
try:
pageflags_enum = self._context.symbol_space.get_enumeration(
self.get_symbol_table_name() + constants.BANG + "pageflags"
).choices
except exceptions.SymbolError:
vollog.debug(
"Unable to find pageflags enum. This can happen in kernels < 2.6.26 or wrong ISF"
)
# set to empty dict to show that the enum was not found, and so shouldn't be searched for again
pageflags_enum = {}
return pageflags_enum
def get_flags_list(self) -> List[str]:
"""Returns a list of page flags
Returns:
List of page flags
"""
flags = []
for name, value in self.pageflags_enum.items():
if self.flags & (1 << value) != 0:
flags.append(name)
return flags
def to_paddr(self) -> int:
"""Converts a page's virtual address to its physical address using the current physical memory model.
Returns:
int: page physical address
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
vmemmap_start = None
if vmlinux.has_symbol("mem_section"):
# SPARSEMEM_VMEMMAP physical memory model: memmap is virtually contiguous
if vmlinux.has_symbol("vmemmap_base"):
# CONFIG_DYNAMIC_MEMORY_LAYOUT - KASLR kernels >= 4.9
vmemmap_start = vmlinux.object_from_symbol("vmemmap_base")
else:
# !CONFIG_DYNAMIC_MEMORY_LAYOUT
if vmlinux_layer._maxvirtaddr < 57:
# 4-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L4
vmemmap_base_l4 = 0xFFFFEA0000000000
vmemmap_start = vmemmap_base_l4
else:
# 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5
# FIXME: Once 5-level paging is supported, uncomment the following lines and remove the exception
# vmemmap_base_l5 = 0xFFD4000000000000
# vmemmap_start = vmemmap_base_l5
raise exceptions.VolatilityException(
"5-level paging is not yet supported"
)
elif vmlinux.has_symbol("mem_map"):
# FLATMEM physical memory model, typically 32bit
vmemmap_start = vmlinux.object_from_symbol("mem_map")
elif vmlinux.has_symbol("node_data"):
raise exceptions.VolatilityException("NUMA systems are not yet supported")
else:
raise exceptions.VolatilityException("Unsupported Linux memory model")
if not vmemmap_start:
raise exceptions.VolatilityException(
"Something went wrong, we shouldn't be here"
)
page_type_size = vmlinux.get_type("page").size
pagec = vmlinux_layer.canonicalize(self.vol.offset)
pfn = (pagec - vmemmap_start) // page_type_size
page_paddr = pfn * vmlinux_layer.page_size
return page_paddr
def get_content(self) -> Union[str, None]:
"""Returns the page content
Returns:
The page content
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
physical_layer = vmlinux.context.layers["memory_layer"]
page_paddr = self.to_paddr()
if not page_paddr:
return None
page_data = physical_layer.read(page_paddr, vmlinux_layer.page_size)
return page_data
class IDR(objects.StructType):
IDR_BITS = 8
IDR_MASK = (1 << IDR_BITS) - 1
INT_SIZE = 4
MAX_IDR_SHIFT = INT_SIZE * 8 - 1
MAX_IDR_BIT = 1 << MAX_IDR_SHIFT
def idr_max(self, num_layers: int) -> int:
"""Returns the maximum ID which can be allocated given idr::layers
Args:
num_layers: Number of layers
Returns:
Maximum ID for a given number of layers
"""
# Kernel < 4.17
bits = min([self.INT_SIZE, num_layers * self.IDR_BITS, self.MAX_IDR_SHIFT])
return (1 << bits) - 1
def idr_find(self, idr_id: int) -> int:
"""Finds an ID within the IDR data structure.
Based on idr_find_slowpath(), 3.9 <= Kernel < 4.11
Args:
idr_id: The IDR lookup ID
Returns:
A pointer to the given ID element
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
if not vmlinux.get_type("idr_layer").has_member("layer"):
vollog.info(
"Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6"
)
return None
if idr_id < 0:
return None
idr_layer = self.top
if not idr_layer:
return None
n = (idr_layer.layer + 1) * self.IDR_BITS
if idr_id > self.idr_max(idr_layer.layer + 1):
return None
assert n != 0
while n > 0 and idr_layer:
n -= self.IDR_BITS
assert n == idr_layer.layer * self.IDR_BITS
idr_layer = idr_layer.ary[(idr_id >> n) & self.IDR_MASK]
return idr_layer
def _old_kernel_get_entries(self) -> int:
# Kernels < 4.11
cur = self.cur
total = next_id = 0
while next_id < cur:
entry = self.idr_find(next_id)
if entry:
yield entry
total += 1
next_id += 1
def _new_kernel_get_entries(self) -> int:
# Kernels >= 4.11
id_storage = linux.IDStorage.choose_id_storage(
self._context, kernel_module_name="kernel"
)
for page_addr in id_storage.get_entries(root=self.idr_rt):
yield page_addr
def get_entries(self) -> int:
"""Walks the IDR and yield a pointer associated with each element.
Args:
in_use (int, optional): _description_. Defaults to 0.
Yields:
A pointer associated with each element.
"""
if self.has_member("idr_rt"):
# Kernels >= 4.11
get_entries_func = self._new_kernel_get_entries
else:
# Kernels < 4.11
get_entries_func = self._old_kernel_get_entries
for page_addr in get_entries_func():
yield page_addr
@@ -17,6 +17,7 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("_KTHREAD", extensions.KTHREAD)
self.set_type_class("_LIST_ENTRY", extensions.LIST_ENTRY)
self.set_type_class("_EPROCESS", extensions.EPROCESS)
self.set_type_class("_ERESOURCE", extensions.ERESOURCE)
self.set_type_class("_UNICODE_STRING", extensions.UNICODE_STRING)
self.set_type_class("_EX_FAST_REF", extensions.EX_FAST_REF)
self.set_type_class("_TOKEN", extensions.TOKEN)
@@ -39,6 +40,7 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("_VACB", extensions.VACB)
self.set_type_class("_POOL_TRACKER_BIG_PAGES", pool.POOL_TRACKER_BIG_PAGES)
self.set_type_class("_IMAGE_DOS_HEADER", pe.IMAGE_DOS_HEADER)
self.set_type_class("_KTIMER", extensions.KTIMER)
# Might not necessarily defined in every version of windows
self.optional_set_type_class("_IMAGE_NT_HEADERS", pe.IMAGE_NT_HEADERS)
@@ -1,6 +1,18 @@
{
"symbols": {},
"enums": {},
"enums": {
"EventCategory": {
"base": "long",
"constants": {
"EventCategoryReserved": 0,
"EventCategoryHardwareProfileChange": 1,
"EventCategoryDeviceInterfaceChange": 2,
"EventCategoryTargetDeviceChange": 3,
"EventCategoryKernelSoftRestart": 4
},
"size": 4
}
},
"base_types": {
"unsigned long": {
"kind": "int",
@@ -48,19 +60,164 @@
"user_types": {
"_GENERIC_CALLBACK": {
"fields": {
"Callback": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
"Callback": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 8
},
"offset": 8
}
"Associated": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 16
"size": 24
},
"_NOTIFICATION_PACKET": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"DriverObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DRIVER_OBJECT"
}
},
"offset": 16
},
"NotificationRoutine": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 24
}
},
"kind": "struct",
"size": 48
},
"_SHUTDOWN_PACKET": {
"fields": {
"Entry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"DeviceObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DEVICE_OBJECT"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_DBGPRINT_CALLBACK": {
"fields": {
"Function": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 16
}
},
"kind": "struct",
"size": 24
},
"_NOTIFY_ENTRY_HEADER": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"EventCategory": {
"type": {
"kind": "enum",
"name": "EventCategory"
},
"offset": 16
},
"CallbackRoutine": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 32
},
"DriverObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DRIVER_OBJECT"
}
},
"offset": 48
}
},
"kind": "struct",
"size": 64
},
"_REGISTRY_CALLBACK": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"Function": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 40
}
},
"kind": "struct",
"size": 48
},
"_KBUGCHECK_CALLBACK_RECORD": {
"fields": {
@@ -184,10 +341,10 @@
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "mhl by hand",
"datetime": "2019-08-27T18:17:16.417006"
"version": "0.0.2",
"name": "dgmcdona by hand",
"datetime": "2024-02-02T19:32:00.000000"
},
"format": "4.0.0"
}
}
}
@@ -1,6 +1,18 @@
{
"symbols": {},
"enums": {},
"enums": {
"EventCategory": {
"base": "long",
"constants": {
"EventCategoryReserved": 0,
"EventCategoryHardwareProfileChange": 1,
"EventCategoryDeviceInterfaceChange": 2,
"EventCategoryTargetDeviceChange": 3,
"EventCategoryKernelSoftRestart": 4
},
"size": 4
}
},
"base_types": {
"unsigned long": {
"kind": "int",
@@ -46,6 +58,151 @@
}
},
"user_types": {
"_NOTIFICATION_PACKET": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"DriverObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DRIVER_OBJECT"
}
},
"offset": 8
},
"NotificationRoutine": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 12
}
},
"kind": "struct",
"size": 16
},
"_SHUTDOWN_PACKET": {
"fields": {
"Entry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"DeviceObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DEVICE_OBJECT"
}
},
"offset": 8
}
},
"kind": "struct",
"size": 12
},
"_REGISTRY_CALLBACK": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"Function": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 28
}
},
"kind": "struct",
"size": 32
},
"_REGISTRY_CALLBACK_LEGACY": {
"fields": {
"CreateTime": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 28
}
},
"kind": "struct",
"size": 32
},
"_DBGPRINT_CALLBACK": {
"fields": {
"Function": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 8
}
},
"kind": "struct",
"size": 20
},
"_NOTIFY_ENTRY_HEADER": {
"fields": {
"ListEntry": {
"type": {
"kind": "struct",
"name": "nt_symbols!_LIST_ENTRY"
},
"offset": 0
},
"EventCategory": {
"type": {
"kind": "enum",
"name": "EventCategory"
},
"offset": 8
},
"CallbackRoutine": {
"type": {
"kind": "base",
"name": "unsigned int"
},
"offset": 20
},
"DriverObject": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "struct",
"name": "nt_symbols!_DRIVER_OBJECT"
}
},
"offset": 28
}
},
"kind": "struct",
"size": 32
},
"_GENERIC_CALLBACK": {
"fields": {
"Callback": {
@@ -57,10 +214,20 @@
}
},
"offset": 4
},
"Associated": {
"type": {
"kind": "pointer",
"subtype": {
"kind": "base",
"name": "void"
}
},
"offset": 8
}
},
"kind": "struct",
"size": 8
"size": 12
},
"_KBUGCHECK_CALLBACK_RECORD": {
"fields": {
@@ -184,10 +351,10 @@
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "mhl by hand",
"datetime": "2019-08-27T18:17:16.417006"
"version": "0.0.2",
"name": "dgmcdona by hand",
"datetime": "2024-02-02T19:32:00.000000"
},
"format": "4.0.0"
}
}
}
@@ -20,9 +20,10 @@ from volatility3.framework import (
)
from volatility3.framework.interfaces.objects import ObjectInterface
from volatility3.framework.layers import intel
from volatility3.framework.objects import utility
from volatility3.framework.renderers import conversion
from volatility3.framework.symbols import generic
from volatility3.framework.symbols.windows.extensions import kdbg, pe, pool
from volatility3.framework.symbols.windows.extensions import pool
vollog = logging.getLogger(__name__)
@@ -306,16 +307,19 @@ class MMVAD_SHORT(objects.StructType):
raise AttributeError("Unable to find the private memory member")
@property
def Protection(self):
if self.has_member("u"):
return self.u.VadFlags.Protection
elif self.has_member("Core"):
return self.Core.u.VadFlags.Protection
else:
return None
def get_protection(self, protect_values, winnt_protections):
"""Get the VAD's protection constants as a string."""
protect = None
if self.has_member("u"):
protect = self.u.VadFlags.Protection
elif self.has_member("Core"):
protect = self.Core.u.VadFlags.Protection
protect = self.Protection
try:
value = protect_values[protect]
@@ -593,6 +597,38 @@ class UNICODE_STRING(objects.StructType):
String = property(get_string)
class ERESOURCE(objects.StructType):
def is_valid(self) -> bool:
vollog.debug(f"Checking ERESOURCE Validity: {hex(self.vol.offset)}")
if not self._context.layers[self.vol.layer_name].is_valid(self.vol.offset):
return False
sym_table = self.get_symbol_table_name()
waiters_valid = self.SharedWaiters == 0 or self._context.layers[
self.vol.layer_name
].is_valid(
self.SharedWaiters.vol.offset,
self._context.symbol_space.get_type(
sym_table + constants.BANG + "_KSEMAPHORE"
).size,
)
try:
return (
waiters_valid
and self.SystemResourcesList.Flink is not None
and self.SystemResourcesList.Blink is not None
and self.SystemResourcesList.Flink != self.SystemResourcesList.Blink
and self.SystemResourcesList.Flink.Blink == self.vol.offset
and self.SystemResourcesList.Blink.Flink == self.vol.offset
and self.NumberOfSharedWaiters == 0
)
except exceptions.InvalidAddressException:
return False
class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
"""A class for executive kernel processes objects."""
@@ -611,11 +647,23 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
ctime = self.get_create_time()
if not isinstance(ctime, datetime.datetime):
# A process must have a creation time
return False
if not (1998 < ctime.year < 2030):
current_year = datetime.datetime.now().year
if not (1998 < ctime.year < current_year + 10):
return False
etime = self.get_exit_time()
if isinstance(etime, datetime.datetime):
if not (1998 < etime.year < current_year + 10):
return False
# Exit time, if available, must be after the creation time
# At this point, we are sure both are datetimes, so let's compare them
if ctime > etime:
return False
# NT pids are divisible by 4
if self.UniqueProcessId % 4 != 0:
return False
@@ -633,7 +681,11 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
if dtb & ~0xFFF == 0:
return False
## TODO: we can also add the thread Flink and Blink tests if necessary
# Quick smear test on thread Flink and Blink
kernel = 0x80000000 # Yes, it's a quick test
list_head = self.ThreadListHead
if list_head.Flink < kernel or list_head.Blink < kernel:
return False
except exceptions.InvalidAddressException:
return False
@@ -994,6 +1046,83 @@ class TOKEN(objects.StructType):
vollog.log(constants.LOGLEVEL_VVVV, "Broken Token Privileges.")
class KTIMER(objects.StructType):
"""A class for Kernel Timers"""
VALID_TYPES = {
8: "TimerNotificationObject",
9: "TimerSynchronizationObject",
}
def get_signaled(self):
if self.Header.SignalState:
return "Yes"
return "-"
def get_raw_dpc(self):
"""Returns the encoded DPC since it may not look like a pointer after encoding"""
symbol_table_name = self.get_symbol_table_name()
pointer_type = self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "pointer"
)
return self._context.object(
object_type=pointer_type,
layer_name=self.vol.layer_name,
offset=self.Dpc.vol.offset,
)
def valid_type(self):
return self.Header.Type in self.VALID_TYPES
def get_due_time(self):
return "{0:#010x}:{1:#010x}".format(self.DueTime.HighPart, self.DueTime.LowPart)
def get_dpc(self):
"""Return Dpc, and if Windows 7 or later, decode it"""
symbol_table_name = self.get_symbol_table_name()
kvo = self._context.layers[self.vol.native_layer_name].config[
"kernel_virtual_offset"
]
ntkrnlmp = self._context.module(
symbol_table_name,
layer_name=self.vol.native_layer_name,
offset=kvo,
native_layer_name=self.vol.native_layer_name,
)
if ntkrnlmp.has_symbol("KiWaitNever") and ntkrnlmp.has_symbol("KiWaitAlways"):
wait_never = ntkrnlmp.object(
object_type="unsigned long long",
offset=ntkrnlmp.get_symbol("KiWaitNever").address,
)
wait_always = ntkrnlmp.object(
object_type="unsigned long long",
offset=ntkrnlmp.get_symbol("KiWaitAlways").address,
)
low_byte = (wait_never) & 0xFF
entry = utility.rol(self.get_raw_dpc() ^ wait_never, low_byte)
swap_xor = self._context.layers[self.vol.native_layer_name].canonicalize(
self.vol.offset
)
entry = utility.bswap_64(entry ^ swap_xor)
dpc = entry ^ wait_always
symbol_table_name = self.get_symbol_table_name()
kdpc_type = self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "_KDPC"
)
return self._context.object(
object_type=kdpc_type,
layer_name=self.vol.layer_name,
offset=dpc,
)
else:
return self.Dpc
class KTHREAD(objects.StructType):
"""A class for thread control block objects."""
@@ -0,0 +1,51 @@
import logging
from volatility3.framework import exceptions, objects
from volatility3.framework.symbols.windows.extensions import pool
vollog = logging.getLogger(__name__)
class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject):
"""Class for _SHUTDOWN_PACKET objects found in IoSh pools.
This class serves as a base class for all pooled shutdown callback packets.
It exposes a function which sanity-checks structure members.
"""
def is_valid(self) -> bool:
"""
Perform some checks.
"""
try:
if not (
self.Entry.Flink.is_readable()
and self.Entry.Blink.is_readable()
and self.DeviceObject.is_readable()
):
return False
device = self.DeviceObject
if not device or not (device.DriverObject.DriverStart % 0x1000 == 0):
vollog.debug(
f"callback obj 0x{self.vol.offset:x} invalid due to invalid device object"
)
return False
except exceptions.InvalidAddressException:
vollog.debug(
f"callback obj 0x{self.vol.offset:x} invalid due to invalid address access"
)
return False
try:
header = device.get_object_header()
valid = header.NameInfo.Name == "Device"
return valid
except ValueError:
vollog.debug(f"Could not get NameInfo for object at 0x{self.vol.offset:x}")
return False
class_types_x86 = {"_SHUTDOWN_PACKET": _SHUTDOWN_PACKET}

Some files were not shown because too many files have changed in this diff Show More