Merge branch 'develop' into linux_vmcoreinfo_aslr_and_plugin

This commit is contained in:
Gustavo Moreira
2025-01-30 18:12:19 +11:00
132 changed files with 3140 additions and 856 deletions
+50
View File
@@ -0,0 +1,50 @@
name: build-pyinstaller
on:
push:
branches:
- stable
- develop
- 'release/**'
pull_request:
branches:
- stable
- 'release/**'
jobs:
exe:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
- name: Pyinstall executable
run: |
pyinstaller --clean -y vol.spec
pyinstaller --clean -y volshell.spec
- name: Move files
run: |
mv dist/vol.exe vol.exe
mv dist/volshell.exe volshell.exe
- name: Archive
uses: actions/upload-artifact@v4
with:
name: volatility3-pyinstaller
path: |
vol.exe
volshell.exe
README.md
LICENSE.txt
+7 -2
View File
@@ -42,8 +42,13 @@ jobs:
- name: Testing...
run: |
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k test_windows -v
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k test_linux -v
# VolShell
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v
# Volatility
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_windows and not test_windows_volshell" -v
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v
- name: Clean up post-test
run: |
+1 -1
View File
@@ -88,7 +88,7 @@ The latest generated copy of the documentation can be found at: <https://volatil
## Licensing and Copyright
Copyright (C) 2007-2024 Volatility Foundation
Copyright (C) 2007-2025 Volatility Foundation
All Rights Reserved
+1 -1
View File
@@ -167,7 +167,7 @@ master_doc = "index"
# General information about the project.
project = "Volatility 3"
copyright = "2012-2024, Volatility Foundation"
copyright = "2012-2025, Volatility Foundation"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
+18 -8
View File
@@ -23,7 +23,7 @@ Alignment
.. _Array:
Array
This represents a list of items, which can be access by an index, which is zero-based (meaning the first
This represents a list of items, which can be accessed by an index, which is zero-based (meaning the first
element has index 0). Items in arrays are almost always the same size (it is not a generic list, as in python)
even if they are :ref:`pointers<pointer>` to different sized objects.
@@ -43,7 +43,14 @@ Dereference
.. _Domain:
Domain
This the grouping for input values for a mapping or mathematical function.
The set of input values for a mapping or mathematical function.
I
-
.. _Intermediate Symbol File (ISF):
Intermediate Symbol File (ISF)
They contain kernel structures and specific offsets formatted as JSON. For macOS and Linux analysis, the kernel needs to be added as an ISF file to the volatility 3 symbols directory. For Windows, the required ISF file can often be generated from PDB files automatically downloaded from Microsoft servers, and therefore does not require manual intervention.
M
-
@@ -54,9 +61,7 @@ Map, mapping
of the :ref:`Range<range>`). Mappings can be seen as a mathematical function, and therefore volatility 3
attempts to use mathematical functional notation where possible. Within volatility a mapping is most often
used to refer to the function for translating addresses from a higher layer (domain) to a lower layer (range).
For further information, please see
`Function (mathematics) in wikipedia https://en.wikipedia.org/wiki/Function_(mathematics)`
For further information, please see `Function (mathematics) in Wikipedia<https://en.wikipedia.org/wiki/Function_(mathematics)>_`.
.. _Member:
@@ -69,7 +74,7 @@ O
.. _Object:
Object
This has a specific meaning within computer programming (as in Object Oriented Programming), but within the world
This has a specific meaning within computer programming (as in object-oriented programming), but within the world
of Volatility it is used to refer to a type that has been associated with a chunk of data, or a specific instance
of a type. See also :ref:`Type<type>`.
@@ -116,6 +121,11 @@ Page Table
possible to use them as a way to map a particular address within a (potentially larger, but sparsely populated)
virtual space to a concrete (and usually contiguous) physical space, through the process of :ref:`mapping<map>`.
.. _Plugin:
Plugin
Plugins are the "functions" of the volatility framework. They carry out algorithms on data stored in layers using objects constructed from symbols. Broadly, plugins take in a number of TranslationLayers (the data, which is a representation of part of an image, in a specified type described by templates) and outputs a TreeGrid.
.. _Pointer:
Pointer
@@ -145,9 +155,9 @@ Struct, Structure
Symbol
This is used in many different contexts, as a short term for many things. Within Volatility, a symbol is a
construct that usually encompasses a specific type :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
construct that usually encompasses a specific :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
representing a particular instance of that type within the memory of a compiled and running program. An example
would be the location in memory of a list of active tcp endpoints maintained by the networking stack
would be the location in memory of a list of active TCP endpoints maintained by the networking stack
within an operating system.
T
+62 -36
View File
@@ -41,24 +41,36 @@ to be able to run properly. Any that are defined as optional need not necessari
@classmethod
def get_requirements(cls):
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.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
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.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
),
]
This is a classmethod, because it is called before the specific plugin object has been instantiated (in order to know how
This is a classmethod, so it can be called before the specific plugin object has been instantiated (in order to know how
to instantiate the plugin). At the moment these requirements are fairly straightforward:
::
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name = 'kernel',
description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement specifies the need for a particular submodule. Each module requires a
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a
@@ -85,9 +97,11 @@ not be requested directly from the user.
::
requirements.TranslationLayerRequirement(name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]),
requirements.TranslationLayerRequirement(
name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement indicates that the plugin will operate on a single
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
@@ -110,8 +124,10 @@ not be requested directly from the user.
::
requirements.SymbolTableRequirement(name = "nt_symbols",
description = "Windows kernel symbols"),
requirements.SymbolTableRequirement(
name = "nt_symbols",
description = "Windows kernel symbols"
),
This requirement specifies the need for a particular
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
@@ -127,10 +143,12 @@ not be requested directly from the user.
::
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True),
requirements.ListRequirement(
name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True
),
The next requirement is a List Requirement, populated by integers. The description will be presented to the user to
describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value
@@ -138,9 +156,11 @@ being defined within the configuration tree at all.
::
requirements.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
requirements.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
)
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major
@@ -180,16 +200,24 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces.
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),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str)],
self._generator(pslist.PsList.list_processes(self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str),
],
self._generator(
pslist.PsList.list_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func
)
)
)
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if
@@ -281,5 +309,3 @@ such as ``<table>!_UNICODE``) and the parameters to that type.
Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native
type string) and could not have been a special Structure such as ``_UNICODE``. For the format hint columns, the format
hint type must be used to ensure the error checking does not fail.
+57 -4
View File
@@ -36,7 +36,7 @@ operating system mode for volshell, and the current layer available for use.
(primary) >>>
Volshell itself in essentially a plugin, but an interactive one. As such, most values are accessed through `self`
Volshell itself is essentially a plugin, but an interactive one. As such, most values are accessed through `self`
although there is also a `context` object whenever a context must be provided.
The prompt for the tool will indicate the name of the current layer (which can be accessed as `self.current_layer`
@@ -92,7 +92,7 @@ It can also be provided with an object and will interpret the data for each in t
0x2e8 : UniqueProcessId symbol_table_name1!pointer 4
...
These values can be accessed directory as attributes
These values can be accessed directly as attributes
::
@@ -180,15 +180,68 @@ used:
layer = cc(mynewlayer.MyNewLayer, on_top_of = 'primary', other_parameter = 'important')
with open('output.dmp', 'wb') as fp:
for i in range(0, 1073741824, 0x1000):
for i in range(0, 0x4000000, 0x1000):
data = layer.read(i, 0x1000, pad = True)
fp.write(data)
As this demonstrates, all of the python is accessible, as are the volshell built in functions (such as `cc` which
creates a constructable, like a layer or a symbol table).
User Convenience
----------------
There are functions available that make often-done tasks easier, and generally provide a shell-like experience. These can be listed using `help()` which, as already mentioned, is advertised when volshell starts.
Loading files
-------------
^^^^^^^^^^^^^
Files can be loaded as physical layers using the `load_file` or `lf` command, which takes a filename or a URI. This will be added
to `context.layers` and can be accessed by the name returned by `lf`.
Regex
^^^^^
It is easy to scan for some bytes or a pattern using `regex_scan` or `rx`.
::
(layer_name) >>> rx(rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+")
0x880001400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x8800014000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x8800014000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x8800014000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x8800014000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x8800014000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0x880001769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x880001769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x880001769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x880001769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x880001769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x880001769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff814000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff814000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff814000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff814000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff814000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff81769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff81769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff81769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff81769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff81769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
An optional size can be given for the displayed results as with the other fuctions (db, dw, dd, dq, etc).
You can, of course, specify a different layer name as well.
+26 -21
View File
@@ -1,7 +1,15 @@
[project]
name = "volatility3"
description = "Memory forensics framework"
keywords = ["volatility", "memory", "forensics", "framework", "windows", "linux", "volshell"]
keywords = [
"volatility",
"memory",
"forensics",
"framework",
"windows",
"linux",
"volshell",
]
readme = "README.md"
authors = [
{ name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" },
@@ -10,9 +18,7 @@ requires-python = ">=3.8.0"
license = { text = "VSL" }
dynamic = ["version"]
dependencies = [
"pefile>=2024.8.26",
]
dependencies = ["pefile>=2024.8.26"]
[project.optional-dependencies]
full = [
@@ -20,18 +26,20 @@ full = [
"capstone>=5.0.3,<6",
"pycryptodome>=3.21.0,<4",
"leechcorepyc>=2.19.2,<3; sys_platform != 'darwin'",
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
# 10.0.0 dropped support for Python3.7
# 11.0.0 dropped support for Python3.8, which is still supported by Volatility3
"pillow>=10.0.0,<11.0.0",
]
cloud = [
"gcsfs>=2024.10.0",
"s3fs>=2024.10.0",
]
cloud = ["gcsfs>=2024.10.0", "s3fs>=2024.10.0"]
dev = [
"volatility3[full,cloud]",
"jsonschema>=4.23.0,<5",
"pyinstaller>=6.11.0,<7",
"pyinstaller>=6.5.0,<7",
"pyinstaller-hooks-contrib>=2024.9",
"types-jsonschema>=4.23.0,<5",
]
test = [
@@ -43,8 +51,8 @@ test = [
docs = [
"volatility3[dev]",
"sphinx>=8.0.0,<7",
"sphinx-autodoc-typehints>=2.5.0,<3",
"sphinx>=4.0.0,<9",
"sphinx-autodoc-typehints>=2.0.0,<3",
"sphinx-rtd-theme>=3.0.1,<4",
]
@@ -68,25 +76,22 @@ include = ["volatility3*"]
mypy_path = "./stubs"
show_traceback = true
[tool.mypy.overrides]
ignore_missing_imports = true
[tool.ruff]
line-length = 88
target-version = "py38"
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
]
ignore = [
"E501", # ignore due to conflict with formatter
"E501", # ignore due to conflict with formatter
]
[build-system]
+86 -7
View File
@@ -39,7 +39,9 @@ def runvol(args, volatility, python):
return p.returncode, stdout, stderr
def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]):
def runvol_plugin(plugin, img, volatility, python, pluginargs=None, globalargs=None):
pluginargs = pluginargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
@@ -54,13 +56,68 @@ def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[])
return runvol(args, volatility, python)
def runvolshell(img, volshell, python, volshellargs=None, globalargs=None):
volshellargs = volshellargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
"--single-location",
img,
"-q",
]
+ volshellargs
)
return runvol(args, volshell, python)
#
# TESTS
#
def basic_volshell_test(image, volatility, python, globalargs):
# Basic VolShell test to verify requirements and ensure VolShell runs without crashing
volshell_commands = [
"print(ps())",
"exit()",
]
# FIXME: When the minimum Python version includes 3.12, replace the following with:
# with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
fd, filename = tempfile.mkstemp(suffix=".txt")
try:
volshell_script = "\n".join(volshell_commands)
with os.fdopen(fd, "w") as f:
f.write(volshell_script)
rc, out, _err = runvolshell(
img=image,
volshell=volatility,
python=python,
volshellargs=["--script", filename],
globalargs=globalargs,
)
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
assert rc == 0
assert out.count(b"\n") >= 4
return out
# WINDOWS
def test_windows_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-w"])
assert out.count(b"<EPROCESS") > 40
def test_windows_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
out = out.lower()
@@ -332,6 +389,11 @@ def test_windows_vadyarascan_yara_string(image, volatility, python):
# LINUX
def test_linux_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-l"])
assert out.count(b"<task_struct") > 100
def test_linux_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("linux.pslist.PsList", image, volatility, python)
@@ -646,6 +708,24 @@ def test_linux_page_cache_inodepages(image, volatility, python):
inode_address = hex(0x88001AB5C270)
inode_dump_filename = f"inode_{inode_address}.dmp"
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
image,
volatility,
python,
pluginargs=["--inode", inode_address],
)
assert rc == 0
assert out.count(b"\n") > 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
try:
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
@@ -656,13 +736,8 @@ def test_linux_page_cache_inodepages(image, volatility, python):
)
assert rc == 0
assert out.count(b"\n") > 4
assert out.count(b"\n") >= 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
assert os.path.exists(inode_dump_filename)
with open(inode_dump_filename, "rb") as fp:
inode_contents = fp.read()
@@ -770,6 +845,10 @@ def test_linux_hidden_modules(image, volatility, python):
# MAC
def test_mac_volshell(image, volatility, python):
basic_volshell_test(image, volatility, python, globalargs=["-m"])
def test_mac_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("mac.pslist.PsList", image, volatility, python)
out = out.lower()
+27 -12
View File
@@ -19,7 +19,7 @@ import os
import sys
import tempfile
import traceback
from typing import Any, Dict, List, Tuple, Type, Union
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from urllib import parse, request
try:
@@ -64,7 +64,7 @@ class PrintedProgress:
def __init__(self):
self._max_message_len = 0
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
"""A simple function for providing text-based feedback.
.. warning:: Only for development use.
@@ -81,7 +81,7 @@ class PrintedProgress:
class MuteProgress(PrintedProgress):
"""A dummy progress handler that produces no output when called."""
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
pass
@@ -363,10 +363,21 @@ class CommandLine:
metavar="PLUGIN",
)
for plugin in sorted(plugin_list):
# First line of a plugin docstring will be the short description for -h.
# Text after the first two consecutive new lines will be
# the additional description (argparse epilog).
short_help = additional_help = None
if plugin_list[plugin].__doc__ is not None:
doc_split = plugin_list[plugin].__doc__.split("\n\n", 1)
short_help = doc_split[0].strip()
if len(doc_split) > 1:
additional_help = doc_split[1].strip()
plugin_parser = subparser.add_parser(
plugin,
help=plugin_list[plugin].__doc__,
description=plugin_list[plugin].__doc__,
help=short_help,
description=short_help,
epilog=additional_help,
)
self.populate_requirements_argparse(plugin_parser, plugin_list[plugin])
@@ -572,6 +583,8 @@ class CommandLine:
fulltrace = traceback.TracebackException.from_exception(excp).format(chain=True)
vollog.debug("".join(fulltrace))
file_a_bug_msg = f"Please re-run with -vvv and file a bug with the output at {constants.BUG_URL}"
if isinstance(excp, exceptions.InvalidAddressException):
general = "Volatility was unable to read a requested page:"
if isinstance(excp, exceptions.SwappedInvalidAddressException):
@@ -616,9 +629,7 @@ class CommandLine:
elif isinstance(excp, exceptions.LayerException):
general = f"Volatility experienced a layer-related issue: {excp.layer_name}"
detail = f"{excp}"
caused_by = [
"A faulty layer implementation (re-run with -vvv and file a bug)"
]
caused_by = [f"A faulty layer implementation. {file_a_bug_msg}"]
elif isinstance(excp, exceptions.MissingModuleException):
general = f"Volatility could not import a necessary module: {excp.module}"
detail = f"{excp}"
@@ -629,13 +640,17 @@ class CommandLine:
general = "Volatility experienced an issue when rendering the output:"
detail = f"{excp}"
caused_by = ["An invalid renderer option, such as no visible columns"]
elif isinstance(excp, exceptions.VersionMismatchException):
general = "A version mismatch was detected between two components:"
detail = f"{excp}"
caused_by = [
excp.failure_reason or "An outdated API caller, such as a method.",
file_a_bug_msg,
]
else:
general = "Volatility encountered an unexpected situation."
detail = ""
caused_by = [
"Please re-run using with -vvv and file a bug with the output",
f"at {constants.BUG_URL}",
]
caused_by = [file_a_bug_msg]
# Code that actually renders the exception
output = sys.stderr
+5 -4
View File
@@ -1,7 +1,8 @@
import logging
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
import re
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
vollog = logging.getLogger(__name__)
@@ -67,14 +68,14 @@ class ColumnFilter:
) -> None:
self.column_num = column_num
self.pattern = pattern
self.exclude = exclude
self.regex = regex
self.exclude = exclude
def find(self, item) -> bool:
"""Identifies whether an item is found in the appropriate column"""
try:
if self.regex:
return re.search(self.pattern, f"{item}")
return bool(re.search(self.pattern, f"{item}"))
return self.pattern in f"{item}"
except OSError:
return False
+15 -16
View File
@@ -240,7 +240,7 @@ class Volshell(interfaces.plugins.PluginInterface):
return None
return self.context.modules[self.current_kernel_name]
def change_layer(self, layer_name: str = None):
def change_layer(self, layer_name: Optional[str] = None):
"""Changes the current default layer"""
if not layer_name:
layer_name = self.current_layer
@@ -250,7 +250,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_layer = layer_name
sys.ps1 = f"({self.current_layer}) >>> "
def change_symbol_table(self, symbol_table_name: str = None):
def change_symbol_table(self, symbol_table_name: Optional[str] = None):
"""Changes the current_symbol_table"""
if not symbol_table_name:
print("No symbol table provided, not changing current symbol table")
@@ -262,7 +262,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_symbol_table = symbol_table_name
print(f"Current Symbol Table: {self.current_symbol_table}")
def change_kernel(self, kernel_name: str = None):
def change_kernel(self, kernel_name: Optional[str] = None):
if not kernel_name:
print("No kernel module name provided, not changing current kernel")
if kernel_name not in self.context.modules:
@@ -347,7 +347,7 @@ class Volshell(interfaces.plugins.PluginInterface):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if not isinstance(
@@ -479,7 +479,7 @@ class Volshell(interfaces.plugins.PluginInterface):
if treegrid is not None:
self.render_treegrid(treegrid)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
print("No symbol table provided")
@@ -553,17 +553,16 @@ class Volshell(interfaces.plugins.PluginInterface):
if argname in kwargs:
del kwargs[argname]
for keyword in kwargs:
val = kwargs[keyword]
if not isinstance(
val, interfaces.configuration.BasicTypes
) and not isinstance(val, list):
if not isinstance(val, list) or all(
isinstance(x, interfaces.configuration.BasicTypes) for x in val
):
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
for keyword, val in kwargs.items():
BasicType_or_list_of_BasicType = False # excludes list of lists
if isinstance(val, interfaces.configuration.BasicTypes):
BasicType_or_list_of_BasicType = True
if all(isinstance(x, interfaces.configuration.BasicTypes) for x in val):
BasicType_or_list_of_BasicType = True
if not BasicType_or_list_of_BasicType:
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
self.context.config[config_path + "." + keyword] = val
constructed = clazz(self.context, config_path, **constructor_args)
+81 -4
View File
@@ -2,7 +2,8 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from enum import Enum
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -10,6 +11,16 @@ from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
# Could import the enum from psscan.py to avoid code duplication
class DescExitStateEnum(Enum):
"""Enum for linux task exit_state as defined in include/linux/sched.h"""
TASK_RUNNING = 0x00000000
EXIT_DEAD = 0x00000010
EXIT_ZOMBIE = 0x00000020
EXIT_TRACE = EXIT_ZOMBIE | EXIT_DEAD
class Volshell(generic.Volshell):
"""Shell environment to directly interact with a linux memory image."""
@@ -20,7 +31,7 @@ class Volshell(generic.Volshell):
name="kernel", description="Linux kernel module"
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
@@ -40,6 +51,71 @@ class Volshell(generic.Volshell):
return None
print(f"No task with task ID {pid} found")
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Return the task_struct object that matches the pid. If a physical or a virtual address is provided, construct the task_struct object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID to search for
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: task_struct Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
kernel_layer_name = vmlinux.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
task_struct_symbol = vmlinux.symbol_table_name + constants.BANG + "task_struct"
if virtaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
if physaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
if physaddr is not None or virtaddr is not None:
try:
DescExitStateEnum(task.exit_state)
except ValueError:
print(
f"task_struct @ {hex(task.vol.offset)} as exit_state {task.exit_state} is likely not valid"
)
if not (0 < task.pid < 65535):
print(
f"task_struct @ {hex(task.vol.offset)} as pid {task.pid} is likely not valid"
)
return task
if pid is not None:
tasks = self.list_tasks()
for task in tasks:
if task.pid == pid:
return task
print(f"No task with task ID {pid} found")
return None
def list_tasks(self):
"""Returns a list of task objects from the primary layer"""
# We always use the main kernel memory and associated symbols
@@ -50,6 +126,7 @@ class Volshell(generic.Volshell):
result += [
(["ct", "change_task", "cp"], self.change_task),
(["lt", "list_tasks", "ps"], self.list_tasks),
(["gp", "get_process", "get_task"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
@@ -61,7 +138,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -69,7 +146,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+3 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -63,7 +63,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -71,7 +71,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+59 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -44,11 +44,67 @@ class Volshell(generic.Volshell):
)
)
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Returns the _EPROCESS object that matches the pid. If a physical or a virtual address is provided, construct the _EPROCESS object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID / UniqueProcessId to search for.
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: _EPROCESS Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
kernel_layer_name = kernel.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
eprocess_symbol = kernel.symbol_table_name + constants.BANG + "_EPROCESS"
if virtaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
return eproc
if physaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
return eproc
if pid is not None:
processes = self.list_processes()
for process in processes:
if process.UniqueProcessId == pid:
return process
print(f"No process with process ID {pid} found")
return None
return None
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(["cp", "change_process"], self.change_process),
(["lp", "list_processes", "ps"], self.list_processes),
(["gp", "get_process"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
@@ -60,7 +116,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -68,7 +124,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+73 -6
View File
@@ -5,17 +5,30 @@
# Check the python version to ensure it's suitable
import glob
import sys
from volatility3.framework import check_python_version as check_python_version
import zipfile
import importlib
import inspect
import logging
import os
import traceback
from typing import Any, Dict, Generator, List, Tuple, Type, TypeVar
import functools
import warnings
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar
from volatility3.framework import constants, interfaces
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
if (
sys.version_info.major != constants.REQUIRED_PYTHON_VERSION[0]
or sys.version_info.minor < constants.REQUIRED_PYTHON_VERSION[1]
or (
sys.version_info.minor == constants.REQUIRED_PYTHON_VERSION[1]
and sys.version_info.micro < constants.REQUIRED_PYTHON_VERSION[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {'.'.join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater"
)
# ##
#
@@ -53,12 +66,67 @@ def require_interface_version(*args) -> None:
)
class Deprecation:
"""Deprecation related methods."""
@staticmethod
def deprecated_method(
replacement: Callable,
replacement_version: Tuple[int, int, int] = None,
additional_information: str = "",
):
"""A decorator for marking functions as deprecated.
Args:
replacement: The replacement function overriding the deprecated API, in the form of a Callable (typically a method)
replacement_version: The "replacement" base class version that the deprecated method expects before proxying to it. This implies that "replacement" is a method from a class that inherits from VersionableInterface.
additional_information: Information appended at the end of the deprecation message
"""
def decorator(deprecated_func):
@functools.wraps(deprecated_func)
def wrapper(*args, **kwargs):
nonlocal replacement, replacement_version, additional_information
# Prevent version mismatches between deprecated (proxy) methods and the ones they proxy
if (
replacement_version is not None
and callable(replacement)
and hasattr(replacement, "__self__")
):
replacement_base_class = replacement.__self__
# Verify that the base class inherits from VersionableInterface
if inspect.isclass(replacement_base_class) and issubclass(
replacement_base_class,
interfaces.configuration.VersionableInterface,
):
# SemVer check
if not requirements.VersionRequirement.matches_required(
replacement_version, replacement_base_class.version
):
raise exceptions.VersionMismatchException(
deprecated_func,
replacement_base_class,
replacement_version,
"This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.",
)
deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}"
warnings.warn(deprecation_msg, FutureWarning)
# Return the wrapped function with its original arguments
return deprecated_func(*args, **kwargs)
return wrapper
return decorator
class NonInheritable:
def __init__(self, value: Any, cls: Type) -> None:
self.default_value = value
self.cls = cls
def __get__(self, obj: Any, get_type: Type = None) -> Any:
def __get__(self, obj: Any, get_type: Type = Optional[None]) -> Any:
if type is self.cls:
if hasattr(self.default_value, "__get__"):
return self.default_value.__get__(obj, get_type)
@@ -185,8 +253,7 @@ def _zipwalk(path: str):
zip_results[os.path.join(path, os.path.dirname(file.filename))] = (
dirlist
)
for value in zip_results:
yield value, zip_results[value]
yield from zip_results.items()
def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
+5
View File
@@ -76,6 +76,11 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
elif "init_level4_pgt" in table.symbols:
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_level4_pgt"
elif "pkmap_count" in table.symbols and table.get_symbol(
"pkmap_count"
).type.count in (512, 2048):
layer_class = intel.LinuxIntelPAE
dtb_symbol_name = "swapper_pg_dir"
else:
layer_class = intel.LinuxIntel
dtb_symbol_name = "swapper_pg_dir"
@@ -376,8 +376,74 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
valid_kernel = (virtual_layer_name, address, res[0])
return valid_kernel
def method_low_stub_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
# This method is only valid for x64 systems
if not isinstance(vlayer, intel.Intel32e):
return None
kernel_hint = 0
kernel_base = 0
physical_layer = context.layers.get("memory_layer")
# Try locating kernel base via x64 Low Stub in lower 1MB starting from second page (4KB)
# If "Discard Low Memory" setting is disabled in BIOS, the Low Stub may be at the third/fourth or further pages
for offset in range(0x1000, 0x100000, 0x1000):
try:
jmp_and_completion_values = int.from_bytes(
physical_layer.read(offset, 0x8), "little"
)
if (
0xFFFFFFFFFFFF00FF & jmp_and_completion_values
!= constants.windows.JMP_AND_COMPLETION_SIGNATURE
):
continue
cr3_value = int.from_bytes(
physical_layer.read(
offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8
),
"little",
)
# Compare previously observed valid page table address that's stored in vlayer._initial_entry
# with PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3
# which was observed to be an invalid page address, so add 1 (to make it valid too)
if (cr3_value + 1) != vlayer._initial_entry:
continue
potential_kernel_hint = int.from_bytes(
physical_layer.read(
offset
+ constants.windows.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET,
0x8,
),
"little",
)
if 0x3 & potential_kernel_hint:
continue
kernel_hint = potential_kernel_hint & 0xFFFFFFFFFFFF
kernel_base = kernel_hint & (~0x1FFFFF) & 0xFFFFFFFFFFFF
break
except exceptions.InvalidAddressException:
continue
if kernel_base:
# Scanning 32mb in 2mb chunks for the 'ntoskrnl' base address
while (kernel_base + 0x2000000) > kernel_hint:
for i in range(0, 0x200000, 0x1000):
valid_kernel = self.check_kernel_offset(
context, vlayer, kernel_base, progress_callback
)
if valid_kernel:
return valid_kernel
kernel_base -= 0x200000
return None
# List of methods to be run, in order, to determine the valid kernels
methods = [
method_low_stub_offset,
method_kdbg_offset,
method_module_offset,
method_fixed_mapping,
+3 -1
View File
@@ -166,7 +166,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
cls,
context: interfaces.context.ContextInterface,
initial_layer: str,
stack_set: List[Type[interfaces.automagic.StackerLayerInterface]] = None,
stack_set: Optional[
List[Type[interfaces.automagic.StackerLayerInterface]]
] = None,
progress_callback: constants.ProgressCallback = None,
):
"""Stacks as many possible layers on top of the initial layer as can be done.
@@ -104,9 +104,11 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
for subclazz in framework.class_subclasses(IdentifierProcessor):
self._classifiers[subclazz.operating_system] = subclazz
@abstractmethod
def add_identifier(self, location: str, operating_system: str, identifier: str):
"""Adds an identifier to the store"""
@abstractmethod
def find_location(
self, identifier: bytes, operating_system: Optional[str]
) -> Optional[str]:
@@ -120,15 +122,18 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
The location of the symbols file that matches the identifier
"""
@abstractmethod
def get_local_locations(self) -> Iterable[str]:
"""Returns a list of all the local locations"""
@abstractmethod
def update(self):
"""Locates all files under the symbol directories. Updates the cache with additions, modifications and removals.
This also updates remote locations based on a cache timeout.
"""
@abstractmethod
def get_identifier_dictionary(
self, operating_system: Optional[str] = None, local_only: bool = False
) -> Dict[bytes, str]:
@@ -142,12 +147,15 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
A dictionary of identifiers mapped to a location
"""
@abstractmethod
def get_identifier(self, location: str) -> Optional[bytes]:
"""Returns an identifier based on a specific location or None"""
@abstractmethod
def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]:
"""Returns all identifiers for a particular operating system"""
@abstractmethod
def get_location_statistics(
self, location: str
) -> Optional[Tuple[int, int, int, int]]:
@@ -157,6 +165,7 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
A tuple of base_types, types, enums, symbols, or None is location not found
"""
@abstractmethod
def get_hash(self, location: str) -> Optional[str]:
"""Returns the hash of the JSON from within a location ISF"""
@@ -292,6 +301,13 @@ class SqliteCache(CacheManagerInterface):
This also updates remote locations based on a cache timeout.
"""
if progress_callback is None:
def dummy_progress(*args, **kargs) -> None:
return None
progress_callback = dummy_progress
on_disk_locations = set(
[
filename
@@ -1,14 +0,0 @@
import sys
required_python_version = (3, 8, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {required_python_version[0]}.{required_python_version[1]}.{required_python_version[2]} or greater"
)
@@ -11,7 +11,7 @@ expect to be in the context (such as particular layers or symboltables).
import abc
import logging
import os
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type
from urllib import parse, request
from volatility3.framework import constants, interfaces
@@ -314,11 +314,11 @@ class TranslationLayerRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: interfaces.configuration.ConfigSimpleType = None,
optional: bool = False,
oses: List = None,
architectures: List = None,
oses: Optional[List] = None,
architectures: Optional[List[str]] = None,
) -> None:
"""Constructs a Translation Layer Requirement.
@@ -526,18 +526,18 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
component: Optional[Type[interfaces.configuration.VersionableInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
if version is None:
raise TypeError("Version cannot be None")
if component is None:
raise TypeError("Component cannot be 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
)
if component is None:
raise TypeError("Component cannot be None")
self._component: Type[interfaces.configuration.VersionableInterface] = component
self._version = version
@@ -546,7 +546,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
context: interfaces.context.ContextInterface,
config_path: str,
accumulator: Optional[
List[interfaces.configuration.VersionableInterface]
Set[interfaces.configuration.VersionableInterface]
] = None,
) -> Dict[str, interfaces.configuration.RequirementInterface]:
# Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type
@@ -580,7 +580,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
)
if result:
result.update({config_path: self})
result[config_path] = self
return result
context.config[interfaces.configuration.path_join(config_path, self.name)] = (
@@ -604,10 +604,10 @@ class PluginRequirement(VersionRequirement):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
plugin: Type[interfaces.plugins.PluginInterface] = None,
plugin: Optional[Type[interfaces.plugins.PluginInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
super().__init__(
@@ -627,7 +627,7 @@ class ModuleRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
architectures: Optional[List[str]] = None,
optional: bool = False,
@@ -23,6 +23,8 @@ from volatility3.framework.constants._version import (
VERSION_SUFFIX as VERSION_SUFFIX,
)
REQUIRED_PYTHON_VERSION = (3, 8, 0)
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")),
+1 -1
View File
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 13 # Number of changes that only add to the interface
VERSION_MINOR = 19 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -6,6 +6,7 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import IntEnum, Flag
from dataclasses import dataclass
KERNEL_NAME = "__kernel__"
@@ -358,3 +359,57 @@ VMCOREINFO_MAGIC = b"VMCOREINFO\x00"
# Aligned to 4 bytes. See storenote() in kernels < 4.19 or append_kcore_note() in kernels >= 4.19
VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00"
OSRELEASE_TAG = b"OSRELEASE="
@dataclass
class TaintFlag:
shift: int
desc: str
when_present: bool
module: bool
TAINT_FLAGS = {
"P": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=True, module=True
),
"G": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=False, module=True
),
"F": TaintFlag(shift=1 << 1, desc="FORCED_MODULE", when_present=True, module=False),
"S": TaintFlag(
shift=1 << 2, desc="CPU_OUT_OF_SPEC", when_present=True, module=False
),
"R": TaintFlag(shift=1 << 3, desc="FORCED_RMMOD", when_present=True, module=False),
"M": TaintFlag(shift=1 << 4, desc="MACHINE_CHECK", when_present=True, module=False),
"B": TaintFlag(shift=1 << 5, desc="BAD_PAGE", when_present=True, module=False),
"U": TaintFlag(shift=1 << 6, desc="USER", when_present=True, module=False),
"D": TaintFlag(shift=1 << 7, desc="DIE", when_present=True, module=False),
"A": TaintFlag(
shift=1 << 8, desc="OVERRIDDEN_ACPI_TABLE", when_present=True, module=False
),
"W": TaintFlag(shift=1 << 9, desc="WARN", when_present=True, module=False),
"C": TaintFlag(shift=1 << 10, desc="CRAP", when_present=True, module=True),
"I": TaintFlag(
shift=1 << 11, desc="FIRMWARE_WORKAROUND", when_present=True, module=False
),
"O": TaintFlag(shift=1 << 12, desc="OOT_MODULE", when_present=True, module=True),
"E": TaintFlag(
shift=1 << 13, desc="UNSIGNED_MODULE", when_present=True, module=True
),
"L": TaintFlag(shift=1 << 14, desc="SOFTLOCKUP", when_present=True, module=False),
"K": TaintFlag(shift=1 << 15, desc="LIVEPATCH", when_present=True, module=True),
"X": TaintFlag(shift=1 << 16, desc="AUX", when_present=True, module=True),
"T": TaintFlag(shift=1 << 17, desc="RANDSTRUCT", when_present=True, module=True),
"N": TaintFlag(shift=1 << 18, desc="TEST", when_present=True, module=True),
}
"""Flags used to taint kernel and modules, for debugging purposes.
Map based on 6.12-rc5.
Documentation :
- https://www.kernel.org/doc/Documentation/admin-guide/sysctl/kernel.rst#:~:text=guide/sysrq.rst.-,tainted,-%3D%3D%3D%3D%3D%3D%3D%0A%0ANon%2Dzero%20if
- https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst#:~:text=More%20detailed%20explanation%20for%20tainting
- taint_flag kernel struct
- taint_flags kernel constant
"""
@@ -10,3 +10,21 @@ KERNEL_MODULE_NAMES = ["ntkrnlmp", "ntkrnlpa", "ntkrpamp", "ntoskrnl"]
"""The list of names that kernel modules can have within the windows OS"""
PE_MAX_EXTRACTION_SIZE = 1024 * 1024 * 256
"""
The following constants represent the layout of the Low Stub which exists only on x64 machines with no virtualization/emulation,
responsible for transitioning from Real Mode(16 bit) to Protected Mode(32 bit) and Long Mode(64 bit) on boot/return from sleep.
Contains offsets to fields and structures within the undocumented structure _PROCESSOR_START_BLOCK.
Here's a reference: https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/inc/amd64.h#L3334
"""
# Expected signature for validation, constructed from:
# PROCESSOR_START_BLOCK->Jmp->OpCode | PROCESSOR_START_BLOCK->Jmp->Offset | PROCESSOR_START_BLOCK->CompletionFlag
JMP_AND_COMPLETION_SIGNATURE = 0x00000001000600E9
# Address of LmTarget (Long Mode target)
PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = (
0x70 # PROCESSOR_START_BLOCK->LmTarget, PVOID 8 bytes
)
# CR3 register within structures describing initial processor state to be started
PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes
+11 -13
View File
@@ -11,7 +11,8 @@ without them interfering with each other.
import functools
import hashlib
import logging
from typing import Callable, Iterable, List, Optional, Set, Tuple, Union
import re
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
from volatility3.framework import constants, interfaces, symbols, exceptions
from volatility3.framework.objects import templates
@@ -229,7 +230,7 @@ class Module(interfaces.context.ModuleInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -337,7 +338,7 @@ class Module(interfaces.context.ModuleInterface):
)
@property
def symbols(self):
def symbols(self) -> Iterable[str]:
return self.context.symbol_space[self.symbol_table_name].symbols
get_symbol = get_module_wrapper("get_symbol")
@@ -386,10 +387,8 @@ class ModuleCollection(interfaces.context.ModuleContainer):
"""Class to contain a collection of SizedModules and reason about their
contents."""
def __init__(
self, modules: Optional[List[interfaces.context.ModuleInterface]] = None
) -> None:
self._prefix_count = {}
def __init__(self, modules: Optional[List[SizedModule]] = None) -> None:
self._modules: Dict[str, SizedModule] = {}
super().__init__(modules)
def deduplicate(self) -> "ModuleCollection":
@@ -402,20 +401,19 @@ class ModuleCollection(interfaces.context.ModuleContainer):
new_modules = []
seen: Set[str] = set()
for mod in self._modules:
if mod.hash not in seen or mod.size == 0:
if self._modules[mod].hash not in seen or self._modules[mod].size == 0:
new_modules.append(mod)
seen.add(mod.hash) # type: ignore # FIXME: mypy #5107
seen.add(self._modules[mod].hash)
return ModuleCollection(new_modules)
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused module name"""
if prefix not in self._prefix_count:
self._prefix_count[prefix] = 1
existing_names = [name for name in self if re.match(rf"^{prefix}[0-9]*$", name)]
if not existing_names:
return prefix
count = self._prefix_count[prefix]
count = len(existing_names)
while prefix + str(count) in self:
count += 1
self._prefix_count[prefix] = count
return prefix + str(count)
@property
+34 -1
View File
@@ -8,9 +8,10 @@ space or symbol tables, and by layers when an address is invalid. The
:class:`PagedInvalidAddressException` contains information about the
size of the invalid page.
"""
from typing import Dict, Optional
from typing import Callable, Dict, Optional, Tuple
from volatility3.framework import interfaces
from volatility3.framework.interfaces.configuration import VersionableInterface
class VolatilityException(Exception):
@@ -130,3 +131,35 @@ class OfflineException(VolatilityException):
class RenderException(VolatilityException):
"""Thrown if there is an error during rendering"""
class LinuxPageCacheException(VolatilityException):
"""Thrown if there is an error during Linux Page Cache processing"""
class VersionMismatchException(VolatilityException):
"""Thrown if a version mismatch has been encountered between two components."""
def __init__(
self,
source_component: Callable,
target_component: VersionableInterface,
target_version: Tuple[int, int, int],
failure_reason: str = None,
*args,
):
"""
Args:
source_component: The component that required the target component
target_component: The component that is required. Must inherit from VersionableInterface
target_version: The version of the target component that was required, and ultimately was not satisfied
failure_reason: A detailed failure reason to enhance debugging and bug tracking
"""
super().__init__(*args)
self.source_component = source_component
self.target_component = target_component
self.target_version = target_version
self.failure_reason = failure_reason
def __str__(self):
return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet."
@@ -42,7 +42,7 @@ class AutomagicInterface(
priority = 10
"""An ordering to indicate how soon this automagic should be run"""
exclusion_list = []
exclusion_list: List[str] = []
"""A list of plugin categories (typically operating systems) which the plugin will not operate on"""
def __init__(
@@ -53,7 +53,7 @@ ConfigSimpleType = Optional[Union[SimpleTypes, List[SimpleTypes]]]
def path_join(*args) -> str:
"""Joins configuration paths together."""
# If a path element (particularly the first) is empty, then remove it from the list
args = tuple([arg for arg in args if arg])
args = tuple(arg for arg in args if arg)
return CONFIG_SEPARATOR.join(args)
@@ -82,7 +82,7 @@ class HierarchicalDict(collections.abc.Mapping):
def __init__(
self,
initial_dict: Dict[str, "SimpleTypeRequirement"] = None,
initial_dict: Optional[Dict[str, "SimpleTypeRequirement"]] = None,
separator: str = CONFIG_SEPARATOR,
) -> None:
"""
@@ -328,7 +328,7 @@ class RequirementInterface(metaclass=ABCMeta):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: ConfigSimpleType = None,
optional: bool = False,
) -> None:
@@ -618,7 +618,7 @@ class ConstructableRequirementInterface(RequirementInterface):
self,
context: "interfaces.context.ContextInterface",
config_path: str,
requirement_dict: Dict[str, object] = None,
requirement_dict: Optional[Dict[str, object]] = None,
) -> Optional["interfaces.objects.ObjectInterface"]:
"""Constructs the class, handing args and the subrequirements as
parameters to __init__"""
@@ -652,6 +652,7 @@ class ConstructableRequirementInterface(RequirementInterface):
class ConfigurableRequirementInterface(RequirementInterface):
"""Simple Abstract class to provide build_required_config."""
@abstractmethod
def build_configuration(
self,
context: "interfaces.context.ContextInterface",
@@ -771,17 +772,16 @@ class ConfigurableInterface(metaclass=ABCMeta):
str: The newly generated full configuration path
"""
random_config_dict = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
random.SystemRandom().choices(string.ascii_uppercase + string.digits, k=8)
)
new_config_path = path_join(base_config_path, random_config_dict)
# TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in
# This should check that each k corresponds to a requirement and each v is of the appropriate type
# This would require knowledge of the new configurable itself to verify, and they should do validation in the
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a basic type
for k, v in kwargs.items():
if not isinstance(v, (int, str, bool, float, bytes)):
if not isinstance(v, BasicTypes):
raise TypeError(
"Config values passed to make_subconfig can only be simple types"
)
+16 -4
View File
@@ -85,7 +85,7 @@ class ContextInterface(metaclass=ABCMeta):
object_type: Union[str, "interfaces.objects.Template"],
layer_name: str,
offset: int,
native_layer_name: str = None,
native_layer_name: Optional[str] = None,
**arguments,
) -> "interfaces.objects.ObjectInterface":
"""Object factory, takes a context, symbol, offset and optional
@@ -114,6 +114,7 @@ class ContextInterface(metaclass=ABCMeta):
"""
return copy.deepcopy(self)
@abstractmethod
def module(
self,
module_name: str,
@@ -232,7 +233,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -277,27 +278,37 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol = self.get_symbol(name)
return self.offset + symbol.address
@abstractmethod
def get_type(self, name: str) -> "interfaces.objects.Template":
"""Returns a type from the module's symbol table."""
@abstractmethod
def get_symbol(self, name: str) -> "interfaces.symbols.SymbolInterface":
"""Returns a symbol object from the module's symbol table."""
@abstractmethod
def get_enumeration(self, name: str) -> "interfaces.objects.Template":
"""Returns an enumeration from the module's symbol table."""
@abstractmethod
def has_type(self, name: str) -> bool:
"""Determines whether a type is present in the module's symbol table."""
@abstractmethod
def has_symbol(self, name: str) -> bool:
"""Determines whether a symbol is present in the module's symbol table."""
@abstractmethod
def has_enumeration(self, name: str) -> bool:
"""Determines whether an enumeration is present in the module's symbol table."""
def symbols(self) -> List:
"""Lists the symbols contained in the symbol table for this module"""
@property
@abstractmethod
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the symbols contained in the symbol table for this module"""
raise NotImplementedError("Symbols property has not been implemented.")
@abstractmethod
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
"""Returns the symbols within table_name (or this module if not specified) that live at the specified
absolute offset provided."""
@@ -343,6 +354,7 @@ class ModuleContainer(collections.abc.Mapping):
def __iter__(self):
return iter(self._modules)
@abstractmethod
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
+1 -1
View File
@@ -210,7 +210,7 @@ class DataLayerInterface(
context: interfaces.context.ContextInterface,
scanner: ScannerInterface,
progress_callback: constants.ProgressCallback = None,
sections: Iterable[Tuple[int, int]] = None,
sections: Optional[Iterable[Tuple[int, int]]] = None,
) -> Iterable[Any]:
"""Scans a Translation layer by chunk.
@@ -374,6 +374,7 @@ class Template:
f"{self.__class__.__name__} object has no attribute {attr}"
)
@abc.abstractmethod
def __call__(
self,
context: "interfaces.context.ContextInterface",
@@ -183,7 +183,7 @@ class TreeGrid(metaclass=ABCMeta):
@abstractmethod
def populate(
self,
function: VisitorSignature = None,
function: Optional[VisitorSignature] = None,
initial_accumulator: Any = None,
fail_on_errors: bool = True,
) -> Optional[Exception]:
@@ -235,7 +235,7 @@ class TreeGrid(metaclass=ABCMeta):
node: Optional[TreeNode],
function: VisitorSignature,
initial_accumulator: _Type,
sort_key: ColumnSortKey = None,
sort_key: Optional[ColumnSortKey] = None,
) -> None:
"""Visits all the nodes in a tree, calling function on each one.
+12 -4
View File
@@ -122,7 +122,7 @@ class BaseSymbolTableInterface:
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterator of the Symbol names."""
"""Returns an iterable of the available symbol names."""
raise NotImplementedError(
"Abstract property symbols not implemented by subclass."
)
@@ -131,7 +131,7 @@ class BaseSymbolTableInterface:
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the Symbol type names."""
"""Returns an iterable of the available symbol type names."""
raise NotImplementedError(
"Abstract property types not implemented by subclass."
)
@@ -149,7 +149,7 @@ class BaseSymbolTableInterface:
@property
def enumerations(self) -> Iterable[Any]:
"""Returns an iterator of the Enumeration names."""
"""Returns an iterable of the available enumerations."""
raise NotImplementedError(
"Abstract property enumerations not implemented by subclass."
)
@@ -256,6 +256,7 @@ class SymbolSpaceInterface(collections.abc.Mapping):
"""An interface for the container that holds all the symbol-containing
tables for use within a context."""
@abstractmethod
def free_table_name(self, prefix: str = "layer") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
@@ -365,6 +366,7 @@ class NativeTableInterface(BaseSymbolTableInterface):
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the available symbol names."""
return []
def get_enumeration(self, name: str) -> objects.Template:
@@ -373,7 +375,13 @@ class NativeTableInterface(BaseSymbolTableInterface):
)
@property
def enumerations(self) -> Iterable[str]:
def enumerations(self) -> Iterable[Any]:
"""Returns an iterable of the available enumerations."""
return []
@property
def types(self) -> Iterable[str]:
"""Returns an iterable of the available symbol type names."""
return []
@@ -129,6 +129,8 @@ if HAS_LEECHCORE:
def readline(self, __size: Optional[int] = ...) -> bytes:
data = b""
if not __size:
__size = 0
while __size > self._chunk_size or __size < 0:
data += self.read(self._chunk_size)
index = data.find(b"\n")
+1 -1
View File
@@ -194,7 +194,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
) -> None:
super().__init__(context, config_path, name, metadata)
self._base_layer = self.config["base_layer"]
self._pages = self.config.get("pages", None)
self._pages = self.config.get("pages", [])
self._pages_len = len(self._pages)
if not self._pages:
raise PDBFormatException(name, "Invalid/no pages specified")
+10 -4
View File
@@ -140,7 +140,13 @@ class RegistryHive(linear.LinearlyMappedLayer):
"""Returns the appropriate Node, interpreted from the Cell based on its
Signature."""
cell = self.get_cell(cell_offset)
signature = cell.cast("string", max_length=2, encoding="latin-1")
try:
signature = cell.cast("string", max_length=2, encoding="latin-1")
except (RegistryInvalidIndex, exceptions.InvalidAddressException):
vollog.debug(
f"Failed to get cell signature for cell (0x{cell.vol.offset:x})"
)
return cell
if signature == "nk":
return cell.u.KeyNode
elif signature == "sk":
@@ -186,9 +192,9 @@ class RegistryHive(linear.LinearlyMappedLayer):
while key_array and node_key:
subkeys = node_key[-1].get_subkeys()
for subkey in subkeys:
# registry keys are not case sensitive so compare lowercase
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms724946(v=vs.85).aspx
if subkey.get_name().lower() == key_array[0].lower():
# registry keys are not case sensitive so compare likewise
# https://learn.microsoft.com/en-us/windows/win32/sysinfo/structure-of-the-registry
if subkey.get_name().casefold() == key_array[0].casefold():
node_key = node_key + [subkey]
found_key, key_array = found_key + [key_array[0]], key_array[1:]
break
@@ -72,7 +72,7 @@ class MultiStringScanner(layers.ScannerInterface):
return None
for char in value:
trie[char] = trie.get(char, {})
trie.setdefault(char, {})
trie = trie[char]
# Mark the end of a string
+4
View File
@@ -57,6 +57,10 @@ class VmwareLayer(segmented.SegmentedLayer):
)
meta_layer = self.context.layers.get(self._meta_layer, None)
if meta_layer is None:
raise exceptions.LayerException(
self._meta_layer, "VMware: Meta layer not found"
)
header_size = struct.calcsize(self.header_structure)
data = meta_layer.read(0, header_size)
magic, unknown, groupCount = struct.unpack(self.header_structure, data)
+1
View File
@@ -54,6 +54,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
segments = []
self._segment_headers = []
segment_names = None
for sindex in range(ehdr.e_shnum):
shdr = self.context.object(
+3 -3
View File
@@ -152,7 +152,7 @@ class PrimitiveObject(interfaces.objects.ObjectInterface):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
data_format: DataFormatInfo,
new_value: TUnion[int, float, bool, bytes, str] = None,
new_value: Optional[TUnion[int, float, bool, bytes, str]] = None,
**kwargs,
) -> "PrimitiveObject":
"""Creates the appropriate class and returns it so that the native type
@@ -601,7 +601,7 @@ class Enumeration(interfaces.objects.ObjectInterface, int):
inverse_choices[v] = k
return inverse_choices
def lookup(self, value: int = None) -> str:
def lookup(self, value: Optional[int] = None) -> str:
"""Looks up an individual value and returns the associated name.
If multiple identifiers map to the same value, the first matching identifier will be returned
@@ -690,7 +690,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
count: int = 0,
subtype: templates.ObjectTemplate = None,
subtype: Optional[templates.ObjectTemplate] = None,
) -> None:
super().__init__(context=context, type_name=type_name, object_info=object_info)
self._vol["count"] = count
+5 -2
View File
@@ -33,11 +33,12 @@ def array_to_string(
) -> interfaces.objects.ObjectInterface:
"""Takes a volatility Array of characters and returns a string."""
# TODO: Consider checking the Array's target is a native char
if count is None:
count = array.vol.count
if not isinstance(array, objects.Array):
raise TypeError("Array_to_string takes an Array of char")
if count is None:
count = array.vol.count
return array.cast("string", max_length=count, errors=errors)
@@ -45,8 +46,10 @@ def pointer_to_string(pointer: "objects.Pointer", count: int, errors: str = "rep
"""Takes a volatility Pointer to characters and returns a string."""
if not isinstance(pointer, objects.Pointer):
raise TypeError("pointer_to_string takes a Pointer")
if count < 1:
raise ValueError("pointer_to_string requires a positive count")
char = pointer.dereference()
return char.cast("string", max_length=count, errors=errors)
@@ -14,8 +14,8 @@ vollog = logging.getLogger(__name__)
class ConfigWriter(plugins.PluginInterface):
"""Runs the automagics and both prints and outputs configuration in the
output directory."""
"""Runs the automagics and both prints and outputs configuration in the \
output directory."""
_required_framework_version = (2, 0, 0)
+1
View File
@@ -132,6 +132,7 @@ class IsfInfo(plugins.PluginInterface):
valid = check_valid(data)
except (UnicodeDecodeError, json.decoder.JSONDecodeError):
vollog.warning(f"Invalid ISF: {entry}")
continue
yield (
0,
(
+2 -2
View File
@@ -1,8 +1,8 @@
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
"""A module containing a plugin that recovers bash command history
from bash process memory."""
import datetime
import struct
@@ -1,8 +1,8 @@
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
"""A module containing a plugin that verifies the operation function
pointers of network protocols."""
import logging
from typing import List
@@ -5,6 +5,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -27,6 +28,11 @@ class Check_idt(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
@@ -99,8 +105,10 @@ class Check_idt(interfaces.plugins.PluginInterface):
idt_addr = idt_addr & address_mask
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, idt_addr
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, idt_addr
)
)
yield (
@@ -18,6 +18,7 @@ vollog = logging.getLogger(__name__)
class Check_modules(plugins.PluginInterface):
"""Compares module list to sysfs info, if available"""
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
@classmethod
@@ -1,8 +1,7 @@
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
"""A module containing a plugin that checks the system call table for hooks."""
import contextlib
import logging
from typing import List
@@ -83,7 +82,7 @@ class Check_syscall(plugins.PluginInterface):
return table_size
def _get_table_info_disassembly(self, ptr_sz, vmlinux):
def _get_table_info_disassembly(self, ptr_sz, vmlinux) -> int:
"""Find the size of the system call table by disassembling functions
that immediately reference it in their first instruction This is in the
form 'cmp reg,NR_syscalls'."""
@@ -108,9 +107,13 @@ class Check_syscall(plugins.PluginInterface):
return 0
vmlinux = self.context.modules[self.config["kernel"]]
data = self.context.layers.read(vmlinux.layer_name, func_addr, 6)
vmlinux_layer = self.context.layers[vmlinux.layer_name]
try:
data = vmlinux_layer.read(func_addr, 6)
except exceptions.InvalidAddressException:
return 0
for address, size, mnemonic, op_str in md.disasm_lite(data, func_addr):
for _address, _size, mnemonic, op_str in md.disasm_lite(data, func_addr):
if mnemonic == "CMP":
table_size = int(op_str.split(",")[1].strip()) & 0xFFFF
break
+2 -2
View File
@@ -1,8 +1,8 @@
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
"""A module containing a plugin for enumerating memory-mapped
ELF files across all processes."""
import logging
from typing import List, Optional, Type
@@ -18,7 +18,7 @@ class Envars(plugins.PluginInterface):
"""Lists processes with their environment variables"""
_required_framework_version = (2, 13, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls):
@@ -40,8 +40,9 @@ class Envars(plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def get_task_env_variables(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
env_area_max_size: int = 8192,
@@ -0,0 +1,334 @@
# 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 io
from dataclasses import dataclass
from typing import Type, List, Dict, Tuple
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import (
format_hints,
TreeGrid,
NotAvailableValue,
UnreadableValue,
)
from volatility3.framework.objects import utility
from volatility3.framework.constants import architectures
from volatility3.framework.symbols import linux
# Image manipulation functions are kept in the plugin,
# to prevent a general exit on missing PIL (pillow) dependency.
try:
from PIL import Image
has_pil = True
except ImportError:
has_pil = False
vollog = logging.getLogger(__name__)
@dataclass
class Framebuffer:
"""Framebuffer object internal representation. This is useful to unify a framebuffer with precalculated
properties and pass it through functions conveniently."""
id: str
xres_virtual: int
yres_virtual: int
line_length: int
bpp: int
"""Bits Per Pixel"""
size: int
color_fields: Dict[str, Tuple[int, int, int]]
fb_info: interfaces.objects.ObjectInterface
class Fbdev(interfaces.plugins.PluginInterface):
"""Extract framebuffers from the fbdev graphics subsystem"""
_version = (1, 0, 0)
_required_framework_version = (2, 11, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0)
),
requirements.BooleanRequirement(
name="dump",
description="Dump framebuffers",
default=False,
optional=True,
),
]
@classmethod
def parse_fb_pixel_bitfields(
cls, fb_var_screeninfo: interfaces.objects.ObjectInterface
) -> Dict[str, Tuple[int, int, int]]:
"""Organize a framebuffer pixel format into a dictionary.
This is needed to know the position and bitlength of a color inside
a pixel.
Args:
fb_var_screeninfo: a fb_var_screeninfo kernel object instance
Returns:
The color fields mappings
Documentation:
include/uapi/linux/fb.h:
struct fb_bitfield {
__u32 offset; /* beginning of bitfield */
__u32 length; /* length of bitfield */
__u32 msb_right; /* != 0 : Most significant bit is right */
};
"""
# Naturally order by RGBA
color_mappings = [
("R", fb_var_screeninfo.red),
("G", fb_var_screeninfo.green),
("B", fb_var_screeninfo.blue),
("A", fb_var_screeninfo.transp),
]
color_fields = {}
for color_code, fb_bitfield in color_mappings:
color_fields[color_code] = (
int(fb_bitfield.offset),
int(fb_bitfield.length),
int(fb_bitfield.msb_right),
)
return color_fields
@classmethod
def convert_fb_raw_buffer_to_image(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
fb: Framebuffer,
):
"""Convert raw framebuffer pixels to an image.
Args:
fb: the relevant Framebuffer object
Returns:
A PIL Image object
Documentation:
include/uapi/linux/fb.h:
/* Interpretation of offset for color fields: All offsets are from the right,
* inside a "pixel" value, which is exactly 'bits_per_pixel' wide (means: you
* can use the offset as right argument to <<). A pixel afterwards is a bit
* stream and is written to video memory as that unmodified.
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
raw_pixels = io.BytesIO(kernel_layer.read(fb.fb_info.screen_base, fb.size))
bytes_per_pixel = fb.bpp // 8
image = Image.new("RGBA", (fb.xres_virtual, fb.yres_virtual))
# This is not designed to be extremely fast (numpy isn't available),
# but convenient and dynamic for any color field layout.
for y in range(fb.yres_virtual):
for x in range(fb.xres_virtual):
raw_pixel = int.from_bytes(raw_pixels.read(bytes_per_pixel), "little")
pixel = [0, 0, 0, 255]
# The framebuffer is expected to have been correctly constructed,
# especially by parse_fb_pixel_bitfields, to get the needed RGBA mappings.
for i, color_code in enumerate(["R", "G", "B", "A"]):
offset, length, msb_right = fb.color_fields[color_code]
if length == 0:
continue
color_value = (raw_pixel >> offset) & (2**length - 1)
if msb_right:
# Reverse bit order
color_value = int(
"{:0{length}b}".format(color_value, length=length)[::-1], 2
)
pixel[i] = color_value
image.putpixel((x, y), tuple(pixel))
return image
@classmethod
def dump_fb(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
fb: Framebuffer,
convert_to_png_image: bool,
) -> str:
"""Dump a Framebuffer buffer to disk.
Args:
fb: the relevant Framebuffer object
convert_to_image: a boolean specifying if the buffer should be converted to an image
Returns:
The filename of the dumped buffer.
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
id = "N-A" if isinstance(fb.id, NotAvailableValue) else fb.id
base_filename = f"{id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp"
if convert_to_png_image:
image_object = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb)
raw_io_output = io.BytesIO()
image_object.save(raw_io_output, "PNG")
final_fb_buffer = raw_io_output.getvalue()
filename = f"{base_filename}.png"
else:
final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size)
filename = f"{base_filename}.raw"
with open_method(filename) as f:
f.write(final_fb_buffer)
return f.preferred_filename
@classmethod
def parse_fb_info(
cls,
fb_info: interfaces.objects.ObjectInterface,
) -> Framebuffer:
"""Parse an fb_info struct
Args:
fb_info: an fb_info kernel object live instance
Returns:
A Framebuffer object
Documentation:
https://docs.kernel.org/fb/api.html:
- struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format.
Those information can't be directly modified by applications, but can be changed by the driver when an application modifies the format.
- struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode,
as well as other miscellaneous parameters.
"""
id = utility.array_to_string(fb_info.fix.id) or NotAvailableValue()
color_fields = None
# 0 = color, 1 = grayscale, >1 = FOURCC
if fb_info.var.grayscale in [0, 1]:
color_fields = cls.parse_fb_pixel_bitfields(fb_info.var)
# There a lot of tricky pixel formats used by drivers and vendors in include/uapi/linux/videodev2.h.
# As Volatility3 is not a video format converter, it is best to play it safe and let the user parse
# the raw data manually (with ffmpeg for example).
elif fb_info.var.grayscale > 1:
fourcc = linux.LinuxUtilities.convert_fourcc_code(fb_info.var.grayscale)
warn_msg = f"""Framebuffer "{id}" uses a FOURCC pixel format "{fourcc}" that isn't natively supported.
You can try using ffmpeg to decode the raw buffer. Example usage:
"ffmpeg -pix_fmts" to list supported formats, then
"ffmpeg -f rawvideo -video_size {fb_info.var.xres_virtual}x{fb_info.var.yres_virtual} -i <FILENAME>.raw -pix_fmt <FORMAT> output.png"."""
vollog.warning(warn_msg)
# Prefer using the virtual resolution, instead of the visible one.
# This prevents missing non-visible data stored in the framebuffer.
fb = Framebuffer(
id,
xres_virtual=fb_info.var.xres_virtual,
yres_virtual=fb_info.var.yres_virtual,
line_length=fb_info.fix.line_length,
bpp=fb_info.var.bits_per_pixel,
size=fb_info.var.yres_virtual * fb_info.fix.line_length,
color_fields=color_fields,
fb_info=fb_info,
)
return fb
def _generator(self):
if not has_pil:
vollog.error(
"PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml."
)
return None
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
if not kernel.has_symbol("num_registered_fb"):
raise exceptions.SymbolError(
"num_registered_fb",
kernel.symbol_table_name,
"The provided symbol does not exist in the symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.",
)
num_registered_fb = kernel.object_from_symbol("num_registered_fb")
if num_registered_fb < 1:
vollog.info("No registered framebuffer in the fbdev API.")
return None
registered_fb = kernel.object_from_symbol("registered_fb")
fb_info_list = utility.array_of_pointers(
registered_fb,
num_registered_fb,
kernel.symbol_table_name + constants.BANG + "fb_info",
self.context,
)
for fb_info in fb_info_list:
fb = self.parse_fb_info(fb_info)
file_output = "Disabled"
if self.config["dump"]:
try:
file_output = self.dump_fb(
self.context, kernel_name, self.open, fb, bool(fb.color_fields)
)
file_output = str(file_output)
except exceptions.InvalidAddressException as excp:
vollog.error(
f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".'
)
file_output = UnreadableValue()
try:
fb_device_name = utility.pointer_to_string(
fb.fb_info.dev.kobj.name, 256
)
except exceptions.InvalidAddressException:
fb_device_name = NotAvailableValue()
yield (
0,
(
format_hints.Hex(fb.fb_info.screen_base),
fb_device_name,
fb.id,
fb.size,
f"{fb.xres_virtual}x{fb.yres_virtual}",
fb.bpp,
"RUNNING" if fb.fb_info.state == 0 else "SUSPENDED",
file_output,
),
)
def run(self):
columns = [
("Address", format_hints.Hex),
("Device", str),
("ID", str),
("Size", int),
("Virtual resolution", str),
("BPP", int),
("State", str),
("Filename", str),
]
return TreeGrid(
columns,
self._generator(),
)
@@ -16,8 +16,7 @@ class Hidden_modules(interfaces.plugins.PluginInterface):
"""Carves memory to find hidden kernel modules"""
_required_framework_version = (2, 10, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -32,8 +31,9 @@ class Hidden_modules(interfaces.plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def get_modules_memory_boundaries(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Tuple[int]:
@@ -4,6 +4,7 @@
import logging
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -26,6 +27,11 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
@@ -66,8 +72,10 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
):
call_addr = call_back.notifier_call
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, call_addr
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, call_addr
)
)
yield (0, [format_hints.Hex(call_addr), module_name, symbol_name])
+43 -27
View File
@@ -5,7 +5,7 @@ import re
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import Generator, Iterator, List, Tuple
from typing import Generator, Iterator, List, Tuple, Union
from volatility3.framework import (
class_subclasses,
@@ -135,8 +135,14 @@ class ABCKmsg(ABC):
bool: True if the kernel being analyzed fulfill the class requirements.
"""
def get_string(self, addr: int, length: int) -> str:
txt = self._context.layers[self.layer_name].read(addr, length) # type: ignore
def get_string(self, addr: int, length: int) -> Union[str, None]:
layer = self._context.layers[self.layer_name]
if not layer.is_valid(addr, length):
vollog.warning("Failed to read log record at address 0x%x", addr)
return None
txt = layer.read(addr, length)
return txt.decode(encoding="utf8", errors="replace")
def nsec_to_sec_str(self, nsec: int) -> str:
@@ -149,7 +155,7 @@ class ABCKmsg(ABC):
# This might seem insignificant but it could cause some issues
# when compared with userland tool results or when used in
# timelines.
return f"{nsec / 1000000000:lu}.{(nsec % 1000000000) / 1000:06lu}"
return f"{nsec // 1000000000}.{(nsec % 1000000000) // 1000:06}"
def get_timestamp_in_sec_str(self, obj) -> str:
# obj could be log, printk_log or printk_info
@@ -166,7 +172,7 @@ class ABCKmsg(ABC):
def get_caller_text(self, caller_id):
caller_name = "CPU" if caller_id & 0x80000000 else "Task"
caller = f"{caller_name}({caller_id & ~0x80000000:u})"
caller = f"{caller_name}({caller_id & ~0x80000000})"
return caller
def get_prefix(self, obj) -> Tuple[int, int, str, str]:
@@ -263,7 +269,7 @@ class Kmsg_3_5_to_3_11(ABCKmsg):
def _get_log_struct_name(self):
return "log"
def get_text_from_log(self, msg) -> str:
def get_text_from_log(self, msg) -> Union[str, None]:
log_struct_name = self._get_log_struct_name()
log_struct_size = self.vmlinux.get_type(log_struct_name).size
msg_offset = msg.vol.offset + log_struct_size
@@ -272,7 +278,8 @@ class Kmsg_3_5_to_3_11(ABCKmsg):
def get_log_lines(self, msg) -> Generator[str, None, None]:
if msg.text_len > 0:
text = self.get_text_from_log(msg)
yield from text.splitlines()
if text:
yield from text.splitlines()
def get_dict_lines(self, msg) -> Generator[str, None, None]:
if msg.dict_len == 0:
@@ -281,9 +288,13 @@ class Kmsg_3_5_to_3_11(ABCKmsg):
log_struct_name = self._get_log_struct_name()
log_struct_size = self.vmlinux.get_type(log_struct_name).size
dict_offset = msg.vol.offset + log_struct_size + msg.text_len
dict_data = self._context.layers[self.layer_name].read(
dict_offset, msg.dict_len
)
layer = self._context.layers[self.layer_name]
try:
dict_data = layer.read(dict_offset, msg.dict_len)
except exceptions.InvalidAddressException:
vollog.debug("Unable to read kmsg dict from 0x%x", dict_offset)
return None
for chunk in dict_data.split(b"\x00"):
yield " " + chunk.decode()
@@ -317,23 +328,27 @@ class Kmsg_3_5_to_3_11(ABCKmsg):
while cur_idx < end_idx:
msg_offset = log_buf_ptr + cur_idx # type: ignore
msg = self.vmlinux.object(object_type=log_struct_name, offset=msg_offset)
if msg.len == 0:
# As per kernel/printk.c:
# A length == 0 for the next message indicates a wrap-around to
# the beginning of the buffer.
cur_idx = 0
end_idx = log_next_idx
else:
facility, level, timestamp, caller = self.get_prefix(msg)
level_txt = self.get_level_text(level)
facility_txt = self.get_facility_text(facility)
try:
if msg.len == 0:
# As per kernel/printk.c:
# A length == 0 for the next message indicates a wrap-around to
# the beginning of the buffer.
cur_idx = 0
end_idx = log_next_idx
else:
facility, level, timestamp, caller = self.get_prefix(msg)
level_txt = self.get_level_text(level)
facility_txt = self.get_facility_text(facility)
for line in self.get_log_lines(msg):
yield facility_txt, level_txt, timestamp, caller, line
for line in self.get_dict_lines(msg):
yield facility_txt, level_txt, timestamp, caller, line
for line in self.get_log_lines(msg):
yield facility_txt, level_txt, timestamp, caller, line
for line in self.get_dict_lines(msg):
yield facility_txt, level_txt, timestamp, caller, line
cur_idx += msg.len
cur_idx += msg.len
except exceptions.InvalidAddressException:
vollog.warning("Kmsg buffer msg length could not be read")
return
class Kmsg_3_11_to_5_10(Kmsg_3_5_to_3_11):
@@ -399,7 +414,7 @@ class Kmsg_5_10_to_(ABCKmsg):
def symtab_checks(cls, vmlinux) -> bool:
return vmlinux.has_symbol("prb")
def get_text_from_data_ring(self, text_data_ring, desc, info) -> str:
def get_text_from_data_ring(self, text_data_ring, desc, info) -> Union[str, None]:
text_data_sz = text_data_ring.size_bits
text_data_mask = 1 << text_data_sz
@@ -427,7 +442,8 @@ class Kmsg_5_10_to_(ABCKmsg):
def get_log_lines(self, text_data_ring, desc, info) -> Generator[str, None, None]:
text = self.get_text_from_data_ring(text_data_ring, desc, info)
yield from text.splitlines()
if text:
yield from text.splitlines()
def get_dict_lines(self, info) -> Generator[str, None, None]:
dict_text = utility.array_to_string(info.dev_info.subsystem)
@@ -4,6 +4,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
@@ -20,7 +21,7 @@ class Kthreads(plugins.PluginInterface):
"""Enumerates kthread functions"""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 2)
_version = (1, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -30,6 +31,11 @@ class Kthreads(plugins.PluginInterface):
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
),
@@ -88,8 +94,10 @@ class Kthreads(plugins.PluginInterface):
if kthread.has_member("full_name")
else task_name
)
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, threadfn
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, threadfn
)
)
fields = [
+1 -2
View File
@@ -1,8 +1,7 @@
# 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
#
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
"""A module containing a plugin that lists loaded kernel modules."""
import logging
from typing import List, Iterable
@@ -0,0 +1,189 @@
# 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, Iterator
from volatility3.plugins.linux import lsmod, check_modules, hidden_modules
from volatility3.framework import interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures
from volatility3.framework.symbols.linux.utilities import tainting
vollog = logging.getLogger(__name__)
class Modxview(interfaces.plugins.PluginInterface):
"""Centralize lsmod, check_modules and hidden_modules results to efficiently \
spot modules presence and taints."""
_version = (1, 0, 0)
_required_framework_version = (2, 17, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="check_modules",
plugin=check_modules.Check_modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="hidden_modules",
plugin=hidden_modules.Hidden_modules,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="plain_taints",
description="Display the plain taints string for each module.",
optional=True,
default=False,
),
]
@classmethod
def flatten_run_modules_results(
cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True
) -> Iterator[extensions.module]:
"""Flatten a dictionary mapping plugin names and modules list, to a single merged list.
This is useful to get a generic lookup list of all the detected modules.
Args:
run_results: dictionary of plugin names mapping a list of detected modules
deduplicate: remove duplicate modules, based on their offsets
Returns:
Iterator of modules objects
"""
seen_addresses = set()
for modules in run_results.values():
for module in modules:
if deduplicate and module.vol.offset in seen_addresses:
continue
seen_addresses.add(module.vol.offset)
yield module
@classmethod
def run_modules_scanners(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
run_hidden_modules: bool = True,
) -> Dict[str, List[extensions.module]]:
"""Run module scanning plugins and aggregate the results. It is designed
to not operate any inter-plugin results triage.
Args:
run_hidden_modules: specify if the hidden_modules plugin should be run
Returns:
Dictionary mapping each plugin to its corresponding result
"""
kernel = context.modules[kernel_name]
run_results = {}
# lsmod
run_results["lsmod"] = list(lsmod.Lsmod.list_modules(context, kernel_name))
# check_modules
sysfs_modules: dict = check_modules.Check_modules.get_kset_modules(
context, kernel_name
)
## Convert get_kset_modules() offsets back to module objects
run_results["check_modules"] = [
kernel.object(object_type="module", offset=m_offset, absolute=True)
for m_offset in sysfs_modules.values()
]
# hidden_modules
if run_hidden_modules:
known_modules_addresses = set(
context.layers[kernel.layer_name].canonicalize(module.vol.offset)
for module in run_results["lsmod"] + run_results["check_modules"]
)
modules_memory_boundaries = (
hidden_modules.Hidden_modules.get_modules_memory_boundaries(
context, kernel_name
)
)
run_results["hidden_modules"] = list(
hidden_modules.Hidden_modules.get_hidden_modules(
context,
kernel_name,
known_modules_addresses,
modules_memory_boundaries,
)
)
return run_results
def _generator(self):
kernel_name = self.config["kernel"]
run_results = self.run_modules_scanners(self.context, kernel_name)
aggregated_modules = {}
# We want to be explicit on the plugins results we are interested in
for plugin_name in ["lsmod", "check_modules", "hidden_modules"]:
# Iterate over each recovered module
for module in run_results[plugin_name]:
# Use offsets as unique keys, whether a module
# appears in many plugin runs or not
if aggregated_modules.get(module.vol.offset, None) is not None:
# Append the plugin to the list of originating plugins
aggregated_modules[module.vol.offset][1].append(plugin_name)
else:
aggregated_modules[module.vol.offset] = (module, [plugin_name])
for module_offset, (module, originating_plugins) in aggregated_modules.items():
# Tainting parsing capabilities applied to the module
if self.config.get("plain_taints"):
taints = tainting.Tainting.get_taints_as_plain_string(
self.context,
kernel_name,
module.taints,
True,
)
else:
taints = ",".join(
tainting.Tainting.get_taints_parsed(
self.context,
kernel_name,
module.taints,
True,
)
)
yield (
0,
(
module.get_name() or NotAvailableValue(),
format_hints.Hex(module_offset),
"lsmod" in originating_plugins,
"check_modules" in originating_plugins,
"hidden_modules" in originating_plugins,
taints or NotAvailableValue(),
),
)
def run(self):
columns = [
("Name", str),
("Address", format_hints.Hex),
("In procfs", bool),
("In sysfs", bool),
("Hidden", bool),
("Taints", str),
]
return TreeGrid(
columns,
self._generator(),
)
@@ -36,7 +36,7 @@ class MountInfo(plugins.PluginInterface):
"""Lists mount points on processes mount namespaces"""
_required_framework_version = (2, 2, 0)
_version = (1, 2, 3)
_version = (1, 2, 4)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -152,9 +152,11 @@ class MountInfo(plugins.PluginInterface):
if not (
task
and task.fs
and task.fs.root
and task.fs.is_readable()
and task.nsproxy
and task.nsproxy.is_readable()
and task.nsproxy.mnt_ns
and task.nsproxy.mnt_ns.is_readable()
):
# This task doesn't have all the information required.
# It should be a kernel < 2.6.30
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from abc import ABC, abstractmethod
import logging
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from typing import Iterator, List, Tuple
from volatility3 import framework
from volatility3.framework import (
@@ -98,6 +99,20 @@ class AbstractNetfilter(ABC):
f"linux.LinuxUtilities version not suitable: required {linuxutils_required_version} found {linuxutils_current_version}"
)
linux_utilities_modules_required_version = (
Netfilter._required_linux_utilities_modules_version
)
linux_utilities_modules_current_version = (
linux_utilities_modules.Modules._version
)
if not requirements.VersionRequirement.matches_required(
linux_utilities_modules_required_version,
linux_utilities_modules_current_version,
):
raise exceptions.PluginRequirementException(
f"linux_utilities_modules.Modules version not suitable: required {linux_utilities_modules_required_version} found {linux_utilities_modules_current_version}"
)
modules = lsmod.Lsmod.list_modules(context, kernel_module_name)
self.handlers = linux.LinuxUtilities.generate_kernel_handler_info(
context, kernel_module_name, modules
@@ -263,8 +278,10 @@ class AbstractNetfilter(ABC):
"""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
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self._context, self.vmlinux.name, self.handlers, addr
)
)
if module_name == "UNKNOWN":
@@ -677,6 +694,7 @@ class Netfilter(interfaces.plugins.PluginInterface):
_version = (1, 1, 0)
_required_linux_utilities_modules_version = (1, 0, 0)
_required_linuxutils_version = (2, 1, 0)
_required_lsmod_version = (2, 0, 0)
@@ -688,6 +706,11 @@ class Netfilter(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=cls._required_linux_utilities_modules_version,
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=cls._required_lsmod_version
),
@@ -6,9 +6,9 @@ import math
import logging
import datetime
from dataclasses import dataclass, astuple
from typing import List, Set, Type, Iterable
from typing import List, Set, Type, Iterable, Tuple
from volatility3.framework import renderers, interfaces
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
@@ -104,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -147,7 +147,13 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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:
if (
inode
and inode.is_link
and inode.has_member("i_link")
and inode.i_link
and inode.i_link.is_readable()
):
i_link_str = inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
@@ -253,6 +259,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
if not root_inode.is_valid():
continue
if not (root_inode.i_mapping and root_inode.i_mapping.is_readable()):
# Retrieving data from the page cache requires a valid address space
continue
# Inode already processed?
if root_inode_ptr in seen_inodes:
continue
@@ -284,6 +294,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
if not file_inode.is_valid():
continue
if not (file_inode.i_mapping and file_inode.i_mapping.is_readable()):
# Retrieving data from the page cache requires a valid address space
continue
# Inode already processed?
if file_inode_ptr in seen_inodes:
continue
@@ -316,10 +330,12 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
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):
@@ -344,8 +360,8 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time
yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time
@staticmethod
def format_fields_with_headers(headers, generator):
@classmethod
def format_fields_with_headers(cls, headers, generator):
"""Uses the headers type to cast the fields obtained from the generator"""
for level, fields in generator:
formatted_fields = []
@@ -389,7 +405,7 @@ class InodePages(plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -420,8 +436,9 @@ class InodePages(plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def write_inode_content_to_file(
cls,
inode: interfaces.objects.ObjectInterface,
filename: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
@@ -443,28 +460,80 @@ class InodePages(plugins.PluginInterface):
# 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.
inode_size = inode.i_size
try:
with open_method(filename) as f:
inode_size = inode.i_size
f.truncate(inode_size)
file_initialized = False
with open_method(filename) as file_obj:
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:
page_bytes_len = min(max_length, len(page_content))
if (
current_fp >= inode_size
or current_fp + page_bytes_len > inode_size
):
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.offset,
inode_size,
page_idx,
)
f.seek(current_fp)
f.write(page_bytes)
continue
page_bytes = page_content[:page_bytes_len]
if not file_initialized:
# Lazy initialization to avoid truncating the file until we are
# certain there is something to write
file_obj.truncate(inode_size)
file_initialized = True
file_obj.seek(current_fp)
file_obj.write(page_bytes)
except exceptions.LinuxPageCacheException:
vollog.error(
f"Error dumping cached pages for inode at {inode.vol.offset:#x}"
)
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
def _generate_inode_fields(
self,
inode: interfaces.objects.ObjectInterface,
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
) -> Iterable[Tuple[int, int, int, int, bool, str]]:
inode_size = inode.i_size
try:
for page_obj in inode.get_pages():
if page_obj.mapping != inode.i_mapping:
vollog.warning(
f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page"
)
continue
page_vaddr = page_obj.vol.offset
page_paddr = page_obj.to_paddr()
page_mapping_addr = page_obj.mapping
page_index = page_obj.index
page_file_offset = page_index * vmlinux_layer.page_size
dump_safe = (
page_file_offset < inode_size
and page_mapping_addr
and page_mapping_addr.is_readable()
)
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
except exceptions.LinuxPageCacheException:
vollog.warning(f"Page cache for inode at {inode.vol.offset:#x} is corrupt")
def _generator(self):
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
@@ -486,7 +555,6 @@ class InodePages(plugins.PluginInterface):
else:
vollog.error("Unable to find inode with path %s", self.config["find"])
return None
elif self.config["inode"]:
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
else:
@@ -501,27 +569,6 @@ class InodePages(plugins.PluginInterface):
vollog.error("The inode is not a regular file")
return None
inode_size = inode.i_size
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"]:
open_method = self.open
inode_address = inode.vol.offset
@@ -530,6 +577,8 @@ class InodePages(plugins.PluginInterface):
self.write_inode_content_to_file(
inode, filename, open_method, vmlinux_layer
)
else:
yield from self._generate_inode_fields(inode, vmlinux_layer)
def run(self):
headers = [
+33 -26
View File
@@ -21,7 +21,7 @@ class Maps(plugins.PluginInterface):
"""Lists all memory maps for all processes."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 2)
_version = (1, 0, 3)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
@@ -83,18 +83,24 @@ class Maps(plugins.PluginInterface):
Returns:
Yields vmas based on the task and filtered based on the filter function
"""
if task.mm:
for vma in task.mm.get_vma_iter():
if filter_func(vma):
yield vma
else:
vollog.debug(
f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func"
)
else:
mm_pointer = task.mm
if not mm_pointer:
vollog.debug(
f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread."
f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread"
)
return
if not mm_pointer.is_readable():
vollog.error(f"Task {task.pid} has an invalid mm member")
return
for vma in mm_pointer.get_vma_iter():
if filter_func(vma):
yield vma
else:
vollog.debug(
f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func"
)
@classmethod
def vma_dump(
@@ -174,31 +180,32 @@ class Maps(plugins.PluginInterface):
]
# if any of the user supplied addresses would fall within this vma return true
if addrs_in_vma:
return True
else:
return False
return bool(addrs_in_vma)
vma_filter_func = vma_filter_function
for task in tasks:
if not task.mm:
if not (task.mm and task.mm.is_readable()):
continue
name = utility.array_to_string(task.comm)
for vma in self.list_vmas(task, filter_func=vma_filter_func):
flags = vma.get_protection()
page_offset = vma.get_page_offset()
major = 0
minor = 0
inode = 0
if vma.vm_file != 0:
inode_num = None
try:
dentry = vma.vm_file.get_dentry()
if dentry != 0:
inode_object = dentry.d_inode
major = inode_object.i_sb.major
minor = inode_object.i_sb.minor
inode = inode_object.i_ino
inode_ptr = dentry.d_inode
inode_num = inode_ptr.i_ino
major = inode_ptr.i_sb.major
minor = inode_ptr.i_sb.minor
except exceptions.InvalidAddressException:
if not inode_num:
inode_num = 0
major = 0
minor = 0
path = vma.get_name(self.context, task)
file_output = "Disabled"
@@ -238,7 +245,7 @@ class Maps(plugins.PluginInterface):
format_hints.Hex(page_offset),
major,
minor,
inode,
inode_num,
path,
file_output,
),
+20 -6
View File
@@ -34,7 +34,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular linux memory image."""
_required_framework_version = (2, 13, 0)
_version = (4, 0, 0)
_version = (4, 1, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -74,7 +74,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
]
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[Any], bool]:
def create_pid_filter(
cls, pid_list: Optional[List[int]] = None
) -> Callable[[Any], bool]:
"""Constructs a filter function for process IDs.
Args:
@@ -177,6 +179,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
file_output = "VMA start matching task start_code not found"
return file_output
@staticmethod
def _format_cred(cred):
return renderers.NotAvailableValue() if cred is None else cred
def _generator(
self,
pid_filter: Callable[[Any], bool],
@@ -210,16 +216,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
task_fields = self.get_task_fields(task, decorate_comm)
task_uid = self._format_cred(task_fields.uid)
task_gid = self._format_cred(task_fields.gid)
task_euid = self._format_cred(task_fields.euid)
task_egid = self._format_cred(task_fields.egid)
yield 0, (
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
task_fields.uid or renderers.NotAvailableValue(),
task_fields.gid or renderers.NotAvailableValue(),
task_fields.euid or renderers.NotAvailableValue(),
task_fields.egid or renderers.NotAvailableValue(),
task_uid,
task_gid,
task_euid,
task_egid,
task_fields.creation_time or renderers.NotAvailableValue(),
file_output,
)
@@ -248,6 +259,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# Note that the init_task itself is not yielded, since "ps" also never shows it.
for task in init_task.tasks:
if not task.is_valid():
continue
if filter_func(task):
continue
@@ -9,8 +9,7 @@ from volatility3.plugins.linux import pslist
class PsTree(interfaces.plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 13, 0)
_version = (1, 1, 1)
@@ -438,7 +438,7 @@ class Sockstat(plugins.PluginInterface):
"""Lists all network connections for all processes."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 2)
_version = (3, 0, 3)
@classmethod
def get_requirements(cls):
@@ -514,25 +514,28 @@ class Sockstat(plugins.PluginInterface):
fd_num, filp, _full_path = fd_internal.fd_fields
task = fd_internal.task
if not (filp.f_op and filp.f_op.is_readable()):
continue
if filp.f_op not in (sfop_addr, dfop_addr):
continue
dentry = filp.get_dentry()
if not dentry:
if not (dentry and dentry.is_readable()):
continue
d_inode = dentry.d_inode
if not d_inode:
if not (d_inode and d_inode.is_readable()):
continue
socket_alloc = linux.LinuxUtilities.container_of(
d_inode, "socket_alloc", "vfs_inode", vmlinux
)
socket = socket_alloc.socket
if not (socket and socket.sk):
if not socket_alloc:
continue
socket = socket_alloc.socket
if not (socket.sk and socket.sk.is_readable()):
continue
sock = socket.sk.dereference()
sock_type = sock.get_type()
@@ -5,6 +5,7 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.framework import interfaces, renderers, exceptions, constants
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
@@ -29,6 +30,11 @@ class tty_check(plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 0, 0),
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
@@ -79,8 +85,10 @@ class tty_check(plugins.PluginInterface):
recv_buf = tty_dev.ldisc.ops.receive_buf
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, recv_buf
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, recv_buf
)
)
yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name))
@@ -18,7 +18,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
"""Scans all virtual memory areas for tasks using yara."""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 2)
_version = (1, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -105,8 +105,9 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
value,
)
@staticmethod
@classmethod
def get_vma_maps(
cls,
task: interfaces.objects.ObjectInterface,
) -> Iterable[Tuple[int, int]]:
"""Creates a map of start/end addresses for each virtual memory area in a task.
+2 -2
View File
@@ -11,8 +11,8 @@ from volatility3.framework.symbols import mac
class Mount(plugins.PluginInterface):
"""A module containing a collection of plugins that produce data typically
found in Mac's mount command"""
"""A module containing a collection of plugins that produce data typically \
found in Mac's mount command"""
_required_framework_version = (2, 0, 0)
+4 -2
View File
@@ -4,7 +4,7 @@
import datetime
import logging
from typing import Callable, Dict, Iterable, List
from typing import Callable, Dict, Iterable, List, Optional
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
@@ -82,7 +82,9 @@ class PsList(interfaces.plugins.PluginInterface):
return list_tasks
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[int], bool]:
def create_pid_filter(
cls, pid_list: Optional[List[int]] = None
) -> Callable[[int], bool]:
def filter_func(_):
return False
+1 -2
View File
@@ -10,8 +10,7 @@ from volatility3.plugins.mac import pslist
class PsTree(plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 0, 0)
+5 -3
View File
@@ -41,8 +41,8 @@ class TimeLinerInterface(metaclass=abc.ABCMeta):
class Timeliner(interfaces.plugins.PluginInterface):
"""Runs all relevant plugins that provide time related information and
orders the results by time."""
"""Runs all relevant plugins that provide time related information and \
orders the results by time."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
@@ -54,7 +54,9 @@ class Timeliner(interfaces.plugins.PluginInterface):
self.automagics: Optional[List[interfaces.automagic.AutomagicInterface]] = None
@classmethod
def get_usable_plugins(cls, selected_list: List[str] = None) -> List[Type]:
def get_usable_plugins(
cls, selected_list: Optional[List[str]] = None
) -> List[Type]:
# Initialize for the run
plugin_list = list(framework.class_subclasses(TimeLinerInterface))
@@ -543,7 +543,7 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
amcache.get_key("Root\\InventoryDriverBinary") # type: ignore
)
)
except KeyError:
except (KeyError, registry.RegistryFormatException):
# Registry key not found
pass
@@ -554,7 +554,7 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
amcache.get_key("Root\\Programs")
) # type: ignore
}
except KeyError:
except (KeyError, registry.RegistryFormatException):
programs = {}
try:
@@ -564,7 +564,7 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
key=_entry_sort_key,
)
except KeyError:
except (KeyError, registry.RegistryFormatException):
files = []
for program_id, file_entries in itertools.groupby(
@@ -593,7 +593,7 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
amcache.get_key("Root\\InventoryApplication") # type: ignore
)
)
except KeyError:
except (KeyError, registry.RegistryFormatException):
programs = {}
try:
@@ -603,7 +603,7 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
key=_entry_sort_key,
)
except KeyError:
except (KeyError, registry.RegistryFormatException):
files = []
for program_id, file_entries in itertools.groupby(
@@ -8,7 +8,7 @@ from typing import Tuple
from Crypto.Cipher import ARC4, AES
from Crypto.Hash import HMAC
from volatility3.framework import interfaces, renderers
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import registry
from volatility3.framework.symbols.windows import versions
@@ -22,7 +22,7 @@ class Cachedump(interfaces.plugins.PluginInterface):
"""Dumps lsa secrets from memory"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -43,16 +43,16 @@ class Cachedump(interfaces.plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def get_nlkm(
sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool
cls, sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool
):
return lsadump.Lsadump.get_secret_by_name(
sechive, "NL$KM", lsakey, is_vista_or_later
)
@staticmethod
def decrypt_hash(edata: bytes, nlkm: bytes, ch, xp: bool):
@classmethod
def decrypt_hash(cls, edata: bytes, nlkm: bytes, ch, xp: bool):
if xp:
hmac_md5 = HMAC.new(nlkm, ch)
rc4key = hmac_md5.digest()
@@ -69,8 +69,8 @@ class Cachedump(interfaces.plugins.PluginInterface):
data += aes.decrypt(buf)
return data
@staticmethod
def parse_cache_entry(cache_data: bytes) -> Tuple[int, int, int, bytes, bytes]:
@classmethod
def parse_cache_entry(cls, cache_data: bytes) -> Tuple[int, int, int, bytes, bytes]:
(uname_len, domain_len) = unpack("<HH", cache_data[:4])
if len(cache_data[60:62]) == 0:
return (uname_len, domain_len, 0, b"", b"")
@@ -79,9 +79,9 @@ class Cachedump(interfaces.plugins.PluginInterface):
enc_data = cache_data[96:]
return (uname_len, domain_len, domain_name_len, enc_data, ch)
@staticmethod
@classmethod
def parse_decrypted_cache(
dec_data: bytes, uname_len: int, domain_len: int, domain_name_len: int
cls, dec_data: bytes, uname_len: int, domain_len: int, domain_name_len: int
) -> Tuple[str, str, str, bytes]:
"""Get the data from the cache and separate it into the username, domain name, and hash data"""
uname_offset = 72
@@ -140,9 +140,14 @@ class Cachedump(interfaces.plugins.PluginInterface):
if cache_item.Name == "NL$Control":
continue
data = sechive.read(cache_item.Data + 4, cache_item.DataLength)
if data is None:
try:
data = sechive.read(cache_item.Data + 4, cache_item.DataLength)
except exceptions.InvalidAddressException:
continue
if not data:
continue
(
uname_len,
domain_len,
@@ -67,6 +67,7 @@ class CmdScan(interfaces.plugins.PluginInterface):
Args:
conhost_proc: the process object for conhost.exe
size_filter: size above which vads will not be returned
Returns:
A list of tuples of:
@@ -99,8 +100,8 @@ class CmdScan(interfaces.plugins.PluginInterface):
kernel_layer_name: The name of the layer on which to operate
kernel_symbol_table_name: The name of the table containing the kernel symbols
config_path: The config path where to find symbol files
procs: list of process objects
max_history: an initial set of CommandHistorySize values
procs: List of process objects
max_history: An initial set of CommandHistorySize values
Returns:
The conhost process object, the command history structure, a dictionary of properties for
@@ -227,7 +228,6 @@ class CmdScan(interfaces.plugins.PluginInterface):
"data": command_history.CommandCountMax,
}
)
command_history_properties.append(
{
"level": 1,
@@ -236,6 +236,7 @@ class CmdScan(interfaces.plugins.PluginInterface):
"data": "",
}
)
for (
cmd_index,
bucket_cmd,
@@ -352,7 +353,7 @@ class CmdScan(interfaces.plugins.PluginInterface):
def _conhost_proc_filter(self, proc: interfaces.objects.ObjectInterface):
"""
Used to filter to only conhost.exe processes
Used to filter only conhost.exe processes
"""
process_name = utility.array_to_string(proc.ImageFileName)
@@ -53,7 +53,7 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
"""Detects the Direct System Call technique used to bypass EDRs"""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
# DLLs that are expected to host system call invocations
valid_syscall_handlers = ("ntdll.dll", "win32u.dll")
@@ -200,8 +200,8 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
return disasm_bytes, end_inst
@staticmethod
def get_disasm_function(architecture: str) -> Callable:
@classmethod
def get_disasm_function(cls, architecture: str) -> Callable:
"""
Returns the disassembly handler for the given architecture
.detail is used to get full instruction information
@@ -284,8 +284,9 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
return None
@staticmethod
@classmethod
def get_vad_maps(
cls,
task: interfaces.objects.ObjectInterface,
) -> List[Tuple[int, int, str]]:
"""Creates a map of start/end addresses within a virtual address
@@ -310,9 +311,9 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
return vads
@staticmethod
@classmethod
def get_range_path(
ranges: List[Tuple[int, int, str]], address: int
cls, ranges: List[Tuple[int, int, str]], address: int
) -> Optional[str]:
"""
Returns the path for the range holding `address`, if found
@@ -433,6 +434,8 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
proc_layer = self.context.layers[proc_layer_name]
vads = self.get_vad_maps(proc)
if not vads:
continue
# for each valid process, look for malicious syscall invocations
for address, vad_path in self._get_rule_hits(
@@ -19,7 +19,7 @@ vollog = logging.getLogger(__name__)
class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the loaded modules in a particular windows memory image."""
"""Lists the loaded DLLs in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 0)
@@ -39,6 +39,9 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
requirements.VersionRequirement(
name="psscan", component=psscan.PsScan, version=(1, 1, 0)
),
requirements.VersionRequirement(
name="pedump", component=pedump.PEDump, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="info", component=info.Info, version=(1, 0, 0)
),
@@ -53,16 +56,16 @@ 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.StringRequirement(
name="name",
description="Specify a regular expression to match dll name(s)",
optional=True,
),
requirements.BooleanRequirement(
name="ignore-case",
description="Specify case insensitivity for the regular expression name matching",
@@ -75,9 +78,6 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
default=False,
optional=True,
),
requirements.VersionRequirement(
name="pedump", component=pedump.PEDump, version=(1, 0, 0)
),
]
def _generator(self, procs):
@@ -90,12 +90,15 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
kuser = info.Info.get_kuser_structure(
self.context, kernel.layer_name, kernel.symbol_table_name
)
nt_major_version = int(kuser.NtMajorVersion)
nt_minor_version = int(kuser.NtMinorVersion)
# LoadTime only applies to versions higher or equal to Window 7 (6.1 and higher)
dll_load_time_field = (nt_major_version > 6) or (
nt_major_version == 6 and nt_minor_version >= 1
)
for proc in procs:
proc_id = proc.UniqueProcessId
proc_layer_name = proc.add_process_layer()
@@ -135,7 +138,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
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
# and 32-bit version shouldn't have the Quadpart according to MSDN
try:
DllLoadTime = conversion.wintime_to_datetime(
entry.LoadTime.QuadPart
@@ -64,7 +64,7 @@ class DriverScan(interfaces.plugins.PluginInterface):
names associated with a driver
Args:
driver: A Eriver object
driver: A Driver object
Returns:
A tuple of strings of (driver name, service key, driver alt. name)
@@ -76,14 +76,14 @@ class Envars(interfaces.plugins.PluginInterface):
"CurrentControlSet\\Control\\Session Manager\\Environment"
)
sys = True
except KeyError:
with contextlib.suppress(KeyError):
except (KeyError, registry.RegistryFormatException):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key(
"ControlSet001\\Control\\Session Manager\\Environment"
)
sys = True
if sys:
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
try:
value_node_name = node.get_name()
@@ -100,11 +100,11 @@ class Envars(interfaces.plugins.PluginInterface):
continue
## The user-specific variables
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key("Environment")
ntuser = True
if ntuser:
with contextlib.suppress(KeyError):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
try:
value_node_name = node.get_name()
@@ -123,7 +123,7 @@ class Envars(interfaces.plugins.PluginInterface):
## The volatile user variables
try:
key = hive.get_key("Volatile Environment")
except KeyError:
except (KeyError, registry.RegistryFormatException):
continue
try:
for node in key.get_values():
@@ -10,6 +10,7 @@ from typing import List
from volatility3.framework import renderers, interfaces, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import registry
from volatility3.plugins.windows.registry import hivelist
vollog = logging.getLogger(__name__)
@@ -86,10 +87,18 @@ class GetServiceSIDs(interfaces.plugins.PluginInterface):
# Get ControlSet\Services.
try:
services = hive.get_key(r"CurrentControlSet\Services")
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
try:
services = hive.get_key(r"ControlSet001\Services")
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
registry.RegistryFormatException,
):
continue
if services:
@@ -158,7 +158,11 @@ class GetSIDs(interfaces.plugins.PluginInterface):
layers.registry.RegistryFormatException,
):
continue
except (KeyError, exceptions.InvalidAddressException):
except (
KeyError,
exceptions.InvalidAddressException,
layers.registry.RegistryFormatException,
):
continue
return sids
@@ -68,7 +68,12 @@ class Handles(interfaces.plugins.PluginInterface):
if not self.context.layers[virtual].is_valid(handle_table_entry.Object):
return None
fast_ref = handle_table_entry.Object.cast("_EX_FAST_REF")
object_header = fast_ref.dereference().cast("_OBJECT_HEADER")
try:
object_header = fast_ref.dereference().cast("_OBJECT_HEADER")
except exceptions.InvalidAddressException:
return None
object_header.GrantedAccess = handle_table_entry.GrantedAccess
except AttributeError:
# starting with windows 8
@@ -77,16 +82,26 @@ class Handles(interfaces.plugins.PluginInterface):
)
if is_64bit:
if handle_table_entry.ObjectPointerBits == 0:
try:
pointer_bits = handle_table_entry.ObjectPointerBits
except exceptions.InvalidAddressException:
return None
offset = handle_table_entry.ObjectPointerBits << 4
if pointer_bits == 0:
return None
offset = pointer_bits << 4
else:
if handle_table_entry.InfoTable == 0:
try:
info_table = handle_table_entry.InfoTable
except exceptions.InvalidAddressException:
return None
offset = handle_table_entry.InfoTable & ~7
if info_table == 0:
return None
offset = info_table & ~7
# print("LowValue: {0:#x} Magic: {1:#x} Offset: {2:#x}".format(handle_table_entry.InfoTable, magic, offset))
object_header = self.context.object(
@@ -94,7 +109,10 @@ class Handles(interfaces.plugins.PluginInterface):
virtual,
offset=offset,
)
object_header.GrantedAccess = handle_table_entry.GrantedAccessBits
try:
object_header.GrantedAccess = handle_table_entry.GrantedAccessBits
except exceptions.InvalidAddressException:
return None
object_header.HandleValue = handle_value
return object_header
@@ -160,7 +178,7 @@ class Handles(interfaces.plugins.PluginInterface):
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVV,
f"Cannot access _OBJECT_HEADER Name at {objt.vol.offset:#x}",
f"Cannot access _OBJECT_HEADER Name at {ptr.vol.offset:#x}",
)
continue
@@ -226,6 +244,14 @@ class Handles(interfaces.plugins.PluginInterface):
masked_offset = offset & layer_object.maximum_address
for entry in table:
# This triggered a backtrace in many testing samples
# in the level == 0 path
# The code above this calls `is_valid` on the `offset`
# It is sent but then does not validate `entry` before
# sending it to `_get_item`
if not self.context.layers[virtual].is_valid(entry.vol.offset):
continue
if level > 0:
yield from self._make_handle_array(entry, level - 1, depth)
depth += 1
@@ -315,7 +341,7 @@ class Handles(interfaces.plugins.PluginInterface):
try:
obj_name = entry.NameInfo.Name.String
except (ValueError, exceptions.InvalidAddressException):
obj_name = ""
obj_name = None
except exceptions.InvalidAddressException:
vollog.log(
@@ -333,7 +359,7 @@ class Handles(interfaces.plugins.PluginInterface):
format_hints.Hex(entry.HandleValue),
obj_type,
format_hints.Hex(entry.GrantedAccess),
obj_name,
obj_name or renderers.NotAvailableValue(),
),
)
@@ -332,7 +332,7 @@ class Hashdump(interfaces.plugins.PluginInterface):
try:
if hive:
result = hive.get_key(key)
except KeyError:
except (KeyError, registry.RegistryFormatException):
vollog.info(
f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image"
)
@@ -8,7 +8,7 @@ from typing import Optional
from Crypto.Cipher import ARC4, DES, AES
from Crypto.Hash import MD5, SHA256
from volatility3.framework import interfaces, renderers
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import registry
from volatility3.framework.symbols.windows import versions
@@ -81,7 +81,10 @@ class Lsadump(interfaces.plugins.PluginInterface):
if not enc_reg_value:
return None
obf_lsa_key = sechive.read(enc_reg_value.Data + 4, enc_reg_value.DataLength)
try:
obf_lsa_key = sechive.read(enc_reg_value.Data + 4, enc_reg_value.DataLength)
except exceptions.InvalidAddressException:
return None
if not obf_lsa_key:
return None
@@ -120,8 +120,7 @@ class Malfind(interfaces.plugins.PluginInterface):
vadinfo.winnt_protections,
)
write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string
dirty_page_check = False
dirty_page = None
if not write_exec:
"""
# Inspect "PAGE_EXECUTE_READ" VAD pages to detect
@@ -135,12 +134,12 @@ class Malfind(interfaces.plugins.PluginInterface):
try:
# If we have a dirty page in a non writable "EXECUTE" region, it is suspicious.
if proc_layer.is_dirty(page):
dirty_page_check = True
dirty_page = page
break
except exceptions.InvalidAddressException:
# Abort as it is likely that other addresses in the same range will also fail.
break
if not dirty_page_check:
if dirty_page is None:
continue
else:
continue
@@ -152,10 +151,10 @@ class Malfind(interfaces.plugins.PluginInterface):
if cls.is_vad_empty(proc_layer, vad):
continue
if dirty_page_check:
if dirty_page is not None:
# Useful information to investigate the page content with volshell afterwards.
vollog.warning(
f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(page)}",
f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}",
)
data = proc_layer.read(vad.get_start(), 64, pad=True)
yield vad, data
@@ -22,7 +22,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls):
@@ -37,8 +37,9 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
]
@staticmethod
@classmethod
def enumerate_mft_records(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
primary_layer_name: str,
@@ -128,8 +129,9 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
layer_name=layer.name,
)
@staticmethod
@classmethod
def parse_mft_records(
cls,
record_map: Dict[int, Tuple[str, int, int]],
mft_record: interfaces.objects.ObjectInterface,
attr: interfaces.objects.ObjectInterface,
@@ -191,8 +193,9 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
file_name,
)
@staticmethod
@classmethod
def parse_data_record(
cls,
mft_record: interfaces.objects.ObjectInterface,
attr: interfaces.objects.ObjectInterface,
record_map: Dict[int, Tuple[str, int, int]],
@@ -325,7 +328,7 @@ class ADS(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 7, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -343,8 +346,9 @@ class ADS(interfaces.plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def parse_ads_data_records(
cls,
record_map: Dict[int, Tuple[str, int, int]],
mft_record: interfaces.objects.ObjectInterface,
attr: interfaces.objects.ObjectInterface,
@@ -394,7 +398,7 @@ class ResidentData(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 7, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -412,8 +416,9 @@ class ResidentData(interfaces.plugins.PluginInterface):
),
]
@staticmethod
@classmethod
def parse_first_data_records(
cls,
record_map: Dict[int, Tuple[str, int, int]],
mft_record: interfaces.objects.ObjectInterface,
attr: interfaces.objects.ObjectInterface,
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import Generator, Iterable, List
from typing import Generator, Iterable, List, Optional
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
@@ -133,7 +133,7 @@ class Modules(interfaces.plugins.PluginInterface):
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
pids: List[int] = None,
pids: Optional[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
@@ -23,7 +23,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Scans for network objects present 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):
@@ -50,9 +50,9 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
]
@staticmethod
@classmethod
def create_netscan_constraints(
context: interfaces.context.ContextInterface, symbol_table: str
cls, context: interfaces.context.ContextInterface, symbol_table: str
) -> List[poolscanner.PoolConstraint]:
"""Creates a list of Pool Tag Constraints for network objects.
+113 -44
View File
@@ -111,8 +111,21 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
The list of indices at which a 1 was found.
"""
ret = []
# This value is broken in many samples and was causing essentially infinite loops
# Testing showed that 8192 is the current size across all Windows versions
# We give some leeway in case it increases in later versions, while still keeping it sane
# The problematic samples had values that looked like addresses, so in the billions
if bitmap_size_in_byte > 8192 * 10:
return ret
for idx in range(bitmap_size_in_byte):
current_byte = context.layers[layer_name].read(bitmap_offset + idx, 1)[0]
try:
current_byte = context.layers[layer_name].read(bitmap_offset + idx, 1)[
0
]
except exceptions.InvalidAddressException:
continue
current_offs = idx * 8
for bit in range(8):
if current_byte & (1 << bit) != 0:
@@ -154,32 +167,37 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
)
else:
# invalid argument.
return None
return
vollog.debug(f"Current Port: {port}")
# the given port serves as a shifted index into the port pool lists
list_index = port >> 8
truncated_port = port & 0xFF
# constructing port_pool object here so callers don't have to
port_pool = context.object(
net_symbol_table + constants.BANG + "_INET_PORT_POOL",
layer_name=layer_name,
offset=port_pool_addr,
)
try:
# constructing port_pool object here so callers don't have to
port_pool = context.object(
net_symbol_table + constants.BANG + "_INET_PORT_POOL",
layer_name=layer_name,
offset=port_pool_addr,
)
# first, grab the given port's PortAssignment (`_PORT_ASSIGNMENT`)
inpa = port_pool.PortAssignments[list_index]
# first, grab the given port's PortAssignment (`_PORT_ASSIGNMENT`)
inpa = port_pool.PortAssignments[list_index]
# then parse the port assignment list (`_PORT_ASSIGNMENT_LIST`) and grab the correct entry
assignment = inpa.InPaBigPoolBase.Assignments[truncated_port]
# then parse the port assignment list (`_PORT_ASSIGNMENT_LIST`) and grab the correct entry
assignment = inpa.InPaBigPoolBase.Assignments[truncated_port]
except exceptions.InvalidAddressException:
return
if not assignment:
return None
return
# the value within assignment.Entry is a) masked and b) points inside of the network object
# first decode the pointer
netw_inside = cls._decode_pointer(assignment.Entry)
try:
netw_inside = cls._decode_pointer(assignment.Entry)
except exceptions.InvalidAddressException:
return
if netw_inside:
# if the value is valid, calculate the actual object address by subtracting the offset
@@ -188,16 +206,30 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
)
yield curr_obj
try:
next_obj_address = cls._decode_pointer(curr_obj.Next)
except exceptions.InvalidAddressException:
return
# if the same port is used on different interfaces multiple objects are created
# those can be found by following the pointer within the object's `Next` field until it is empty
while curr_obj.Next:
curr_obj = context.object(
obj_name,
layer_name=layer_name,
offset=cls._decode_pointer(curr_obj.Next) - ptr_offset,
)
while next_obj_address:
try:
curr_obj = context.object(
obj_name,
layer_name=layer_name,
offset=next_obj_address - ptr_offset,
)
except exceptions.InvalidAddressException:
return
yield curr_obj
try:
next_obj_address = cls._decode_pointer(curr_obj.Next)
except exceptions.InvalidAddressException:
return
@classmethod
def get_tcpip_module(
cls,
@@ -243,16 +275,25 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
The hash table entries which are _not_ empty
"""
# we are looking for entries whose values are not their own address
# smear sanity check from mass testing
if ht_length > 4096:
return
for index in range(ht_length):
current_addr = ht_offset + index * alignment
current_pointer = context.object(
net_symbol_table + constants.BANG + "pointer",
layer_name=layer_name,
offset=current_addr,
)
try:
current_pointer = context.object(
net_symbol_table + constants.BANG + "pointer",
layer_name=layer_name,
offset=current_addr,
)
except exceptions.InvalidAddressException:
continue
# check if addr of pointer is equal to the value pointed to
if current_pointer.vol.offset == current_pointer:
continue
yield current_pointer
@classmethod
@@ -292,11 +333,15 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
tcpip_symbol_table + constants.BANG + "PartitionCount"
).address
part_table_addr = context.object(
net_symbol_table + constants.BANG + "pointer",
layer_name=layer_name,
offset=tcpip_module_offset + part_table_symbol,
)
try:
part_table_addr = context.object(
net_symbol_table + constants.BANG + "pointer",
layer_name=layer_name,
offset=tcpip_module_offset + part_table_symbol,
)
except exceptions.InvalidAddressException:
vollog.debug("`PartitionTable` not present in memory.")
return
# part_table is the actual partition table offset and consists out of a dynamic amount of _PARTITION objects
part_table = context.object(
@@ -304,10 +349,18 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
layer_name=layer_name,
offset=part_table_addr,
)
part_count = int.from_bytes(
context.layers[layer_name].read(tcpip_module_offset + part_count_symbol, 1),
"little",
)
try:
part_count = int.from_bytes(
context.layers[layer_name].read(
tcpip_module_offset + part_count_symbol, 1
),
"little",
)
except exceptions.InvalidAddressException:
vollog.debug("`PartitionCount` not present in memory.")
return
part_table.Partitions.count = part_count
vollog.debug(
@@ -316,9 +369,21 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
entry_offset = context.symbol_space.get_type(obj_name).relative_child_offset(
"ListEntry"
)
for ctr, partition in enumerate(part_table.Partitions):
try:
partitions = part_table.Partitions
except exceptions.InvalidAddressException:
vollog.debug("Partitions member not present in memory")
return
for ctr, partition in enumerate(partitions):
vollog.debug(f"Parsing partition {ctr}")
if partition.Endpoints.NumEntries > 0:
try:
num_entries = partition.Endpoints.NumEntries
except exceptions.InvalidAddressException:
continue
if num_entries > 0:
for endpoint_entry in cls.parse_hashtable(
context,
layer_name,
@@ -402,6 +467,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
upp_symbol = context.symbol_space.get_symbol(
tcpip_symbol_table + constants.BANG + "UdpPortPool"
).address
upp_addr = context.object(
net_symbol_table + constants.BANG + "pointer",
layer_name=layer_name,
@@ -498,13 +564,16 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# then, towards the UDP and TCP port pools
# first, find their addresses
upp_addr, tpp_addr = cls.find_port_pools(
context,
layer_name,
net_symbol_table,
tcpip_symbol_table,
tcpip_module_offset,
)
try:
upp_addr, tpp_addr = cls.find_port_pools(
context,
layer_name,
net_symbol_table,
tcpip_symbol_table,
tcpip_module_offset,
)
except (exceptions.SymbolError, exceptions.InvalidAddressException):
vollog.debug("Unable to reconstruct port pools")
# create port pool objects at the detected address and parse the port bitmap
upp_obj = context.object(
@@ -158,7 +158,7 @@ class PESymbolFinder:
class PDBSymbolFinder(PESymbolFinder):
"""
PESymbolFinder implementation for PDB modules
PESymbolFinder implementation for PDB modules
"""
def _do_get_address(self, name: str) -> Optional[int]:
@@ -195,7 +195,7 @@ class PDBSymbolFinder(PESymbolFinder):
class ExportSymbolFinder(PESymbolFinder):
"""
PESymbolFinder implementation for PDB modules
PESymbolFinder implementation for PDB modules
"""
def _get_name(self, export: pefile.ExportData) -> Optional[str]:
@@ -244,7 +244,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 7, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
# used for special handling of the kernel PDB file. See later notes
os_module_name = "ntoskrnl.exe"
@@ -300,7 +300,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
base_address: int,
) -> Optional[pefile.PE]:
"""
Attempts to pefile object from the bytes of the PE file
Attempts to create a pefile object from the bytes of the PE file
Args:
pe_table_name: name of the pe types table
@@ -330,9 +330,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return pe_ret
@staticmethod
@classmethod
def range_info_for_address(
ranges: ranges_type, address: int
cls, ranges: ranges_type, address: int
) -> Optional[range_type]:
"""
Helper for getting the range information for an address.
@@ -351,8 +351,8 @@ class PESymbols(interfaces.plugins.PluginInterface):
return None
@staticmethod
def filepath_for_address(ranges: ranges_type, address: int) -> Optional[str]:
@classmethod
def filepath_for_address(cls, ranges: ranges_type, address: int) -> Optional[str]:
"""
Helper to get the file path for an address
@@ -369,8 +369,8 @@ class PESymbols(interfaces.plugins.PluginInterface):
return None
@staticmethod
def filename_for_path(filepath: str) -> str:
@classmethod
def filename_for_path(cls, filepath: str) -> str:
"""
Consistent way to get the filename regardless of platform
@@ -382,8 +382,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
"""
return ntpath.basename(filepath).lower()
@staticmethod
@classmethod
def addresses_for_process_symbols(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
layer_name: str,
@@ -416,8 +417,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return found_symbols
@staticmethod
@classmethod
def path_and_symbol_for_address(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
collected_modules: collected_modules_type,
@@ -733,8 +735,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return found, remaining
@staticmethod
@classmethod
def find_symbols(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
wanted_modules: PESymbolFinder.cached_value_dict,
@@ -775,8 +778,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return found_symbols, missing_symbols
@staticmethod
@classmethod
def get_kernel_modules(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
@@ -837,8 +841,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return found_modules
@staticmethod
@classmethod
def get_vads_for_process_cache(
cls,
vads_cache: Dict[int, ranges_type],
owner_proc: interfaces.objects.ObjectInterface,
) -> Optional[ranges_type]:
@@ -865,8 +870,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
return vads
@staticmethod
@classmethod
def get_proc_vads_with_file_paths(
cls,
proc: interfaces.objects.ObjectInterface,
) -> ranges_type:
"""
@@ -928,8 +934,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
yield proc, proc_layer_name, vads
@staticmethod
@classmethod
def get_process_modules(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
+20 -23
View File
@@ -64,30 +64,27 @@ class PEDump(interfaces.plugins.PluginInterface):
"""
Returns the filename of the dump file or None
"""
try:
file_handle = open_method(file_name)
with open_method(file_name) as file_handle:
try:
dos_header = context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=base,
layer_name=layer_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 (
OSError,
exceptions.VolatilityException,
OverflowError,
ValueError,
) as excp:
vollog.debug(f"Unable to dump PE file at offset {base}: {excp}")
return None
for offset, data in dos_header.reconstruct():
file_handle.seek(offset)
file_handle.write(data)
except (
OSError,
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
return file_handle.preferred_filename
@classmethod
def dump_ldr_entry(
@@ -96,7 +93,7 @@ class PEDump(interfaces.plugins.PluginInterface):
pe_table_name: str,
ldr_entry: interfaces.objects.ObjectInterface,
open_method: Type[interfaces.plugins.FileHandlerInterface],
layer_name: str = None,
layer_name: Optional[str] = None,
prefix: str = "",
) -> Optional[str]:
"""Extracts the PE file referenced an LDR_DATA_TABLE_ENTRY (DLL, kernel module) instance
@@ -127,8 +127,8 @@ class PoolHeaderScanner(interfaces.layers.ScannerInterface):
class PoolScanner(plugins.PluginInterface):
"""A generic pool scanner plugin."""
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -181,9 +181,9 @@ class PoolScanner(plugins.PluginInterface):
),
)
@staticmethod
@classmethod
def builtin_constraints(
symbol_table: str, tags_filter: List[bytes] = None
cls, symbol_table: str, tags_filter: Optional[List[bytes]] = None
) -> List[PoolConstraint]:
"""Get built-in PoolConstraints given a list of pool tags.
@@ -4,7 +4,7 @@
import datetime
import logging
from typing import Callable, Iterator, List, Type
from typing import Callable, Iterator, List, Optional, Type
from volatility3.framework import renderers, interfaces, layers, exceptions, constants
from volatility3.framework.configuration import requirements
@@ -114,7 +114,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
@classmethod
def create_pid_filter(
cls, pid_list: List[int] = None, exclude: bool = False
cls, pid_list: Optional[List[int]] = None, exclude: bool = False
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
"""A factory for producing filter functions that filter based on a list
of process IDs.
@@ -171,7 +171,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
@classmethod
def create_name_filter(
cls, name_list: List[str] = None, exclude: bool = False
cls, name_list: Optional[List[str]] = None, exclude: bool = False
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
"""A factory for producing filter functions that filter based on a list
of process names.
@@ -89,7 +89,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
offset: int = None,
offset: Optional[int] = None,
physical: bool = True,
exclude: bool = False,
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
@@ -14,8 +14,7 @@ vollog = logging.getLogger(__name__)
class PsTree(interfaces.plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
"""Plugin for listing processes in a tree based on their parent process ID."""
_required_framework_version = (2, 0, 0)
@@ -21,11 +21,12 @@ 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."""
"""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've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality
We 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 functionality
# 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.
@@ -12,8 +12,7 @@ from volatility3.plugins.windows import poolscanner, bigpools
class HiveScan(interfaces.plugins.PluginInterface):
"""Scans for registry hives present in a particular windows memory
image."""
"""Scans for registry hives present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@@ -4,7 +4,7 @@
import datetime
import logging
from typing import List, Sequence, Iterable, Tuple, Union
from typing import List, Optional, Sequence, Iterable, Tuple, Union
from volatility3.framework import objects, renderers, exceptions, interfaces, constants
from volatility3.framework.configuration import requirements
@@ -51,7 +51,7 @@ class PrintKey(interfaces.plugins.PluginInterface):
def key_iterator(
cls,
hive: RegistryHive,
node_path: Sequence[objects.StructType] = None,
node_path: Optional[Sequence[objects.StructType]] = None,
recurse: bool = False,
) -> Iterable[
Tuple[
@@ -121,7 +121,7 @@ class PrintKey(interfaces.plugins.PluginInterface):
def _printkey_iterator(
self,
hive: RegistryHive,
node_path: Sequence[objects.StructType] = None,
node_path: Optional[Sequence[objects.StructType]] = None,
recurse: bool = False,
):
"""Method that wraps the more generic key_iterator, to provide output
@@ -242,8 +242,8 @@ class PrintKey(interfaces.plugins.PluginInterface):
self,
layer_name: str,
symbol_table: str,
hive_offsets: List[int] = None,
key: str = None,
hive_offsets: Optional[List[int]] = None,
key: Optional[str] = None,
recurse: bool = False,
):
for hive in hivelist.HiveList.list_hives(
@@ -13,7 +13,7 @@ from typing import Any, Generator, List, Tuple
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers.physical import BufferDataLayer
from volatility3.framework.layers.registry import RegistryHive
from volatility3.framework.layers.registry import RegistryHive, RegistryFormatException
from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols import intermed
from volatility3.plugins.windows.registry import hivelist
@@ -167,10 +167,21 @@ class UserAssist(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfac
self._determine_userassist_type()
userassist_node_path = hive.get_key(
"software\\microsoft\\windows\\currentversion\\explorer\\userassist",
return_list=True,
)
try:
userassist_node_path = hive.get_key(
"software\\microsoft\\windows\\currentversion\\explorer\\userassist",
return_list=True,
)
except RegistryFormatException as e:
vollog.warning(
f"Error accessing UserAssist key in {hive_name} at {hive.hive_offset:#x}: {e}"
)
return None
except KeyError:
vollog.warning(
f"UserAssist key not found in {hive_name} at {hive.hive_offset:#x}"
)
return None
if not userassist_node_path:
vollog.warning("list_userassist did not find a valid node_path (or None)")
@@ -270,7 +270,6 @@ class _ScheduledTasksReader(io.BytesIO):
return val
def read_aligned_bstring_expand_sz(self) -> Optional[str]:
# type: () -> Optional[str]
sz = self.read_aligned_u4()
if sz is None:
return None
@@ -1100,9 +1099,8 @@ class DynamicInfo:
class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Decodes scheduled task information from the Windows registry, including
information about triggers, actions, run times, and creation times.
"""
"""Decodes scheduled task information from the Windows registry, including \
information about triggers, actions, run times, and creation times."""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 0)

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