Merge branch 'volatilityfoundation:develop' into vmayarascan

This commit is contained in:
Eve
2023-10-09 17:16:56 +01:00
committed by GitHub
28 changed files with 1101 additions and 152 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Install Volatility3 test
on: [push, pull_request]
jobs:
install_test:
runs-on: ${{ matrix.host }}
strategy:
fail-fast: false
matrix:
host: [ ubuntu-latest, windows-latest ]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Setup python-pip
run: python -m pip install --upgrade pip
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Install volatility3
run: pip install .
- name: Run volatility3
run: vol --help
+23
View File
@@ -0,0 +1,23 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 200
days-before-issue-close: 60
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 200 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 60 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
exempt-issue-labels: "enhancement,plugin-request,question"
+4
View File
@@ -4,6 +4,10 @@ API Changes
When an addition to the existing API is made, the minor version is bumped.
When an API feature or function is removed or changed, the major version is bumped.
2.5.0
=====
Add in support for specifying a type override for object_from_symbol
2.4.0
=====
Add a `get_size()` method to Windows VAD structures and fix several off-by-one issues when calculating VAD sizes.
+58 -15
View File
@@ -1,7 +1,7 @@
Volatility 3 Basics
===================
Volatility splits memory analysis down to several components:
Volatility splits memory analysis down to several components. The main ones are:
* Memory layers
* Templates and Objects
@@ -13,22 +13,65 @@ which acts as a container for all the various layers and tables necessary to con
Memory layers
-------------
A memory layer is a body of data that can be accessed by requesting data at a specific address. Memory is seen as
sequential when accessed through sequential addresses, however, there is no obligation for the data to be stored
sequentially, and modern processors tend to store the memory in a paged format. Moreover, there is no need for the data
to be stored in an easily accessible format, it could be encoded or encrypted or more, it could be the combination of
two other sources. These are typically handled by programs that process file formats, or the memory manager of the
processor, but these are all translations (either in the geometric or linguistic sense) of the original data.
A memory layer is a body of data that can be accessed by requesting data at a specific address. At its lowest level
this data is stored on a phyiscal medium (RAM) and very early computers addresses locations in memory directly. However,
as the size of memory increased and it became more difficult to manage memory most architectures moved to a "paged" model
of memory, where the available memory is cut into specific fixed-sized pages. To help further, programs can ask for any address
and the processor will look up their (virtual) address in a map, to find out where the (physical) address that it lives at is,
in the actual memory of the system.
In Volatility 3 this is represented by a directed graph, whose end nodes are
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are
specifically called a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
In this way, a raw memory image in the LiME file format and a page file can be
combined to form a single Intel virtual memory layer. When requesting addresses from the Intel layer, it will use the
Intel memory mapping algorithm, along with the address of the directory table base or page table map, to translate that
Volatility can work with these layers as long as it knows the map (so, for example that virtual address `1` looks up at physical
address `9`). The automagic that runs at the start of every volatility session often locates the kernel's memory map, and creates
a kernel virtual layer, which allows for kernel addresses to be looked up and the correct data returned. There can, however, be
several maps, and in general there is a different map for each process (although a portion of the operating system's memory is
usually mapped to the same location across all processes). The maps may take the same address but point to a different part of
physical memory. It also means that two processes could theoretically share memory, but having an virtual address mapped to the
same physical address as another process. See the worked example below for more information.
To translate an address on a layer, call :py:meth:`layer.mapping(offset, length, ignore_errors) <volatility3.framework.interfaces.layers.TranslationLayerInterface.mapping>` and it will return a list of chunks without overlap, in order,
for the requested range. If a portion cannot be mapped, an exception will be thrown unless `ignore_errors` is true. Each
chunk will contain the original offset of the chunk, the translated offset, the original size and the translated size of
the chunk, as well as the lower layer the chunk lives within.
Worked example
^^^^^^^^^^^^^^
The operating system and two programs may all appear to have access to all of physical memory, but actually the maps they each have
mean they each see something different:
.. code-block::
:caption: Memory mapping example
Operating system map Physical Memory
1 -> 9 1 - Free
2 -> 3 2 - OS.4, Process 1.4, Process 2.4
3 -> 7 3 - OS.2
4 -> 2 4 - Free
5 - Free
Process 1 map 6 - Process 1.2, Process 2.3
1 -> 12 7 - OS.3
2 -> 6 8 - Process1.3
3 -> 8 9 - OS.1
4 -> 2 10 - Process2.1
11 - Free
Process 2 map 12 - Process1.1
1 -> 10 13 - Free
2 -> 15 14 - Free
3 -> 6 15 - Process2.2
4 -> 2 16 - Free
In this example, part of the operating system is visible across all processes (although not all processes can write to the memory, there
is a permissions model for intel addressing which is not discussed further here).)
In Volatility 3 mappings are represented by a directed graph of layers, whose end nodes are
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are :py:class:`TranslationLayers <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
In this way, a raw memory image in the LiME file format and a page file can be combined to form a single Intel virtual
memory layer. When requesting addresses from the Intel layer, it will use the Intel memory mapping algorithm, along
with the address of the directory table base or page table map, to translate that
address into a physical address, which will then either be directed towards the swap layer or the LiME layer. Should it
be directed towards the LiME layer, the LiME file format algorithm will be translated to determine where within the file
the data is stored and that will be returned.
be directed towards the LiME layer, the LiME file format algorithm will be translate the new address to determine where
within the file the data is stored. When the :py:meth:`layer.read() <volatility3.framework.interfaces.layers.TranslationLayerInterface.read>`
method is called, the translation is done automatically and the correct data gathered and combined.
.. note:: Volatility 2 had a similar concept, called address spaces, but these could only stack linearly one on top of another.
+99 -45
View File
@@ -21,57 +21,72 @@ import sphinx.ext.apidoc
def setup(app):
volatility_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'volatility3'))
volatility_directory = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "volatility3")
)
source_dir = os.path.abspath(os.path.dirname(__file__))
sphinx.ext.apidoc.main(argv = ['-e', '-M', '-f', '-T', '-o', source_dir, volatility_directory])
sphinx.ext.apidoc.main(
["-e", "-M", "-f", "-T", "-o", source_dir, volatility_directory]
)
# Go through the volatility3.framework.plugins files and change them to volatility3.plugins
for dir, _, files in os.walk(os.path.dirname(__file__)):
for filename in files:
if filename.startswith('volatility3.framework.plugins') and filename != 'volatility3.framework.plugins.rst':
if (
filename.startswith("volatility3.framework.plugins")
and filename != "volatility3.framework.plugins.rst"
):
# Change all volatility3.framework.plugins to volatility3.plugins in the file
# Rename the file
new_filename = filename.replace('volatility3.framework.plugins', 'volatility3.plugins')
new_filename = filename.replace(
"volatility3.framework.plugins", "volatility3.plugins"
)
replace_string = b"Submodules\n----------\n\n.. toctree::\n\n"
submodules = replace_string
# If file already exists, read out the subpackages entries from it add them to the new list
if os.path.exists(os.path.join(dir, new_filename)):
with open(os.path.join(dir, new_filename), 'rb') as newfile:
with open(os.path.join(dir, new_filename), "rb") as newfile:
data = newfile.read()
index = data.find(replace_string)
if index > -1:
submodules = data[index:]
with open(os.path.join(dir, new_filename), 'wb') as newfile:
with open(os.path.join(dir, new_filename), "wb") as newfile:
with open(os.path.join(dir, filename), "rb") as oldfile:
line = oldfile.read()
correct_plugins = line.replace(b'volatility3.framework.plugins', b'volatility3.plugins')
correct_submodules = correct_plugins.replace(replace_string, submodules)
correct_plugins = line.replace(
b"volatility3.framework.plugins", b"volatility3.plugins"
)
correct_submodules = correct_plugins.replace(
replace_string, submodules
)
newfile.write(correct_submodules)
os.remove(os.path.join(dir, filename))
elif filename == 'volatility3.framework.rst':
elif filename == "volatility3.framework.rst":
with open(os.path.join(dir, filename), "rb") as contents:
lines = contents.readlines()
plugins_seen = False
with open(os.path.join(dir, filename), "wb") as contents:
for line in lines:
if b'volatility3.framework.plugins' in line:
if b"volatility3.framework.plugins" in line:
plugins_seen = True
if plugins_seen and line == b'':
contents.write(b' volatility3.plugins')
if plugins_seen and line == b"":
contents.write(b" volatility3.plugins")
contents.write(line)
elif filename == 'volatility3.plugins.rst':
elif filename == "volatility3.plugins.rst":
with open(os.path.join(dir, filename), "rb") as contents:
lines = contents.readlines()
with open(os.path.join(dir, 'volatility3.framework.plugins.rst'), "rb") as contents:
with open(
os.path.join(dir, "volatility3.framework.plugins.rst"), "rb"
) as contents:
real_lines = contents.readlines()
# Process real_lines
for line_index in range(len(real_lines)):
if b'Submodules' in real_lines[line_index]:
if b"Submodules" in real_lines[line_index]:
break
else:
line_index = len(real_lines)
@@ -82,36 +97,52 @@ def setup(app):
for line in lines:
contents.write(line)
for line in submodule_lines:
contents.write(line.replace(b'volatility3.framework.plugins', b'volatility3.plugins'))
contents.write(
line.replace(
b"volatility3.framework.plugins", b"volatility3.plugins"
)
)
# Clear up the framework.plugins page
with open(os.path.join(os.path.dirname(__file__), 'volatility3.framework.plugins.rst'), "rb") as contents:
with open(
os.path.join(os.path.dirname(__file__), "volatility3.framework.plugins.rst"),
"rb",
) as contents:
real_lines = contents.readlines()
with open(os.path.join(os.path.dirname(__file__), 'volatility3.framework.plugins.rst'), "wb") as contents:
with open(
os.path.join(os.path.dirname(__file__), "volatility3.framework.plugins.rst"),
"wb",
) as contents:
for line in real_lines:
if b'volatility3.framework.plugins.' not in line:
if b"volatility3.framework.plugins." not in line:
contents.write(line)
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../.."))
from volatility3.framework import constants
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = '2.0'
needs_sphinx = "2.0"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.todo',
'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.autosectionlabel'
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.autosectionlabel",
]
autosectionlabel_prefix_document = True
@@ -119,7 +150,7 @@ autosectionlabel_prefix_document = True
try:
import sphinx_autodoc_typehints
extensions.append('sphinx_autodoc_typehints')
extensions.append("sphinx_autodoc_typehints")
except ImportError:
# If the autodoc typehints extension isn't available, carry on regardless
pass
@@ -128,17 +159,17 @@ except ImportError:
# templates_path = ['tools/templates']
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = 'Volatility 3'
copyright = '2012-2022, Volatility Foundation'
project = "Volatility 3"
copyright = "2012-2022, 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
@@ -147,7 +178,7 @@ copyright = '2012-2022, Volatility Foundation'
# The full version, including alpha/beta/rc tags.
release = constants.PACKAGE_VERSION
# The short X.Y version.
version = ".".join(release.split('.')[0:2])
version = ".".join(release.split(".")[0:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -180,7 +211,7 @@ add_module_names = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -196,8 +227,8 @@ pygments_style = 'sphinx'
# html_theme = 'pydoctheme'
# html_theme_options = {'collapsiblesidebar': True}
# html_theme_path = ['tools']
html_theme = 'sphinx_rtd_theme'
html_theme_options = {'logo_only': True}
html_theme = "sphinx_rtd_theme"
html_theme_options = {"logo_only": True}
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -216,17 +247,17 @@ html_theme_options = {'logo_only': True}
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '_static/vol.png'
html_logo = "_static/vol.png"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'
html_favicon = "_static/favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@@ -275,17 +306,15 @@ html_static_path = ['_static']
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Volatilitydoc'
htmlhelp_basename = "Volatilitydoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
@@ -294,7 +323,13 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Volatility.tex', 'Volatility 3 Documentation', 'Volatility Foundation', 'manual'),
(
"index",
"Volatility.tex",
"Volatility 3 Documentation",
"Volatility Foundation",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -321,7 +356,15 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [('vol-cli', 'volatility', 'Volatility 3 Documentation', ['Volatility Foundation'], 1)]
man_pages = [
(
"vol-cli",
"volatility",
"Volatility 3 Documentation",
["Volatility Foundation"],
1,
)
]
# If true, show URL addresses after external links.
# man_show_urls = False
@@ -332,8 +375,15 @@ man_pages = [('vol-cli', 'volatility', 'Volatility 3 Documentation', ['Volatilit
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Volatility', 'Volatility 3 Documentation', 'Volatility Foundation', 'Volatility',
'Memory forensics framework.', 'Miscellaneous'),
(
"index",
"Volatility",
"Volatility 3 Documentation",
"Volatility Foundation",
"Volatility",
"Memory forensics framework.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
@@ -349,10 +399,14 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {"python": ("http://docs.python.org/", None)}
# -- Autodoc options -------------------------------------------------------
# autodoc_member_order = 'groupwise'
autodoc_default_options = {'members': True, 'inherited-members': True, 'show-inheritance': True}
autoclass_content = 'both'
autodoc_default_options = {
"members": True,
"inherited-members": True,
"show-inheritance": True,
}
autoclass_content = "both"
+4 -4
View File
@@ -78,10 +78,10 @@ Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memo
The above command helps us to find the memory dump's Darwin kernel version. Now using the above banner we can search for the needed ISF file.
If ISF file cannot be found then, follow the instructions on :ref:`getting-started-macos-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory.
If ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory.
mac.pslist
~~~~~~~~~~~~
~~~~~~~~~~
.. code-block:: shell-session
@@ -107,7 +107,7 @@ mac.pslist
``mac.pslist`` helps us to list the processes which are running, their PIDs and PPIDs.
mac.pstree
~~~~~~~~~~~~
~~~~~~~~~~
.. code-block:: shell-session
@@ -128,7 +128,7 @@ mac.pstree
``mac.pstree`` helps us to display the parent child relationships between processes.
mac.ifconfig
~~~~~~~~~~
~~~~~~~~~~~~
.. code-block:: shell-session
+10 -3
View File
@@ -54,6 +54,12 @@ also be included, which can be found in `volatility3.constants.PLUGINS_PATH`.
volatility3.plugins.__path__ = <new_plugin_path> + constants.PLUGINS_PATH
failures = framework.import_files(volatility3.plugins, True)
.. note::
Volatility uses the `volatility3.plugins` namespace for all plugins (including those in `volatility3.framework.plugins`).
Please ensure you only use `volatility3.plugins` and only ever import plugins from this namespace.
This ensures the ability of users to override core plugins without needing write access to the framework directory.
Once the plugins have been imported, we can interrogate which plugins are available. The
:py:func:`~volatility3.framework.list_plugins` call will
return a dictionary of plugin names and the plugin classes.
@@ -67,9 +73,10 @@ return a dictionary of plugin names and the plugin classes.
Determine what configuration options a plugin requires
------------------------------------------------------
For each plugin class, we can call the classmethod `requirements` on it, which will return a list of objects that
adhere to the :py:class:`~volatility3.framework.interfaces.configuration.RequirementInterface` method. The various
types of Requirement are split roughly in two,
For each plugin class, we can call the classmethod
:py:func:`~volatility3.framework.interfaces.configuration.ConfigurableInterface.get_requirements` on it, which will
return a list of objects that adhere to the :py:class:`~volatility3.framework.interfaces.configuration.RequirementInterface`
method. The various types of Requirement are split roughly in two,
:py:class:`~volatility3.framework.interfaces.configuration.SimpleTypeRequirement` (such as integers, booleans, floats
and strings) and more complex requirements (such as lists, choices, multiple requirements, translation layer
requirements or symbol table requirements). A requirement just specifies a type of data and a name, and must be
+6 -5
View File
@@ -12,7 +12,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
def get_install_requires():
requirements = []
with open("requirements-minimal.txt", "r", encoding = "utf-8") as fh:
with open("requirements-minimal.txt", "r", encoding="utf-8") as fh:
for line in fh.readlines():
stripped_line = line.strip()
if stripped_line == "" or stripped_line.startswith("#"):
@@ -20,6 +20,7 @@ def get_install_requires():
requirements.append(stripped_line)
return requirements
setuptools.setup(
name="volatility3",
description="Memory forensics framework",
@@ -36,12 +37,12 @@ setuptools.setup(
"Documentation": "https://volatility3.readthedocs.io/",
"Source Code": "https://github.com/volatilityfoundation/volatility3",
},
packages=setuptools.find_namespace_packages(
include=["volatility3", "volatility3.*"]
),
package_dir={"volatility3": "volatility3"},
python_requires=">=3.7.0",
include_package_data=True,
exclude_package_data={"": ["development", "development.*"], "development": ["*"]},
packages=setuptools.find_namespace_packages(
exclude=["development", "development.*"]
),
entry_points={
"console_scripts": [
"vol = volatility3.cli:main",
+1 -1
View File
@@ -480,7 +480,7 @@ class Volshell(interfaces.plugins.PluginInterface):
accessor = resources.ResourceAccessor()
with accessor.open(url=location) as fp:
self.__console.runsource(
io.TextIOWrapper(fp.read(), encoding="utf-8"), symbol="exec"
io.TextIOWrapper(fp, encoding="utf-8").read(), symbol="exec"
)
print("\nCode complete")
+4 -1
View File
@@ -224,4 +224,7 @@ def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
def clear_cache(complete=False):
os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME))
try:
os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME))
except FileNotFoundError:
vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache")
+1 -1
View File
@@ -44,7 +44,7 @@ BANG = "!"
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 4 # Number of changes that only add to the interface
VERSION_MINOR = 5 # Number of changes that only add to the interface
VERSION_PATCH = 2 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -234,3 +234,50 @@ BLUETOOTH_PROTOCOLS = (
"HIDP",
"AVDTP",
)
# Ref: include/uapi/linux/capability.h
CAPABILITIES = (
"chown",
"dac_override",
"dac_read_search",
"fowner",
"fsetid",
"kill",
"setgid",
"setuid",
"setpcap",
"linux_immutable",
"net_bind_service",
"net_broadcast",
"net_admin",
"net_raw",
"ipc_lock",
"ipc_owner",
"sys_module",
"sys_rawio",
"sys_chroot",
"sys_ptrace",
"sys_pacct",
"sys_admin",
"sys_boot",
"sys_nice",
"sys_resource",
"sys_time",
"sys_tty_config",
"mknod",
"lease",
"audit_write",
"audit_control",
"setfcap",
"mac_override",
"mac_admin",
"syslog",
"wake_alarm",
"block_suspend",
"audit_read",
"perfmon",
"bpf",
"checkpoint_restore",
)
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1
+10 -3
View File
@@ -272,6 +272,7 @@ class Module(interfaces.context.ModuleInterface):
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
object_type: Optional[Union[str, "interfaces.objects.ObjectInterface"]] = None,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object based on a specific symbol (containing type and
@@ -284,6 +285,7 @@ class Module(interfaces.context.ModuleInterface):
symbol_name: Name of the symbol (within the module) to construct
native_layer_name: Name of the layer in which constructed objects are made (for pointers)
absolute: whether the symbol's address is absolute or relative to the module
object_type: Override for the type from the symobl to use (or if the symbol type is missing)
"""
if constants.BANG not in symbol_name:
symbol_name = self.symbol_table_name + constants.BANG + symbol_name
@@ -299,8 +301,13 @@ class Module(interfaces.context.ModuleInterface):
if not absolute:
offset += self._offset
if symbol_val.type is None:
raise TypeError(f"Symbol {symbol_val.name} has no associated type")
if object_type is None:
if symbol_val.type is None:
raise TypeError(
f"Symbol {symbol_val.name} has no associated type and no object_type specified"
)
else:
object_type = symbol_val.type
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if "layer_name" in kwargs:
@@ -308,7 +315,7 @@ class Module(interfaces.context.ModuleInterface):
# Since type may be a template, we don't just call our own module method
return self._context.object(
object_type=symbol_val.type,
object_type=object_type,
layer_name=self._layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
@@ -253,6 +253,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
object_type: Optional[Union[str, "interfaces.objects.ObjectInterface"]] = None,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object created using the symbol_table_name and layer_name
@@ -262,6 +263,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol_name: The name of a symbol (that must be present in the module's symbol table). The symbol's associated type will be used to construct an object at the symbol's offset.
native_layer_name: The native layer for objects that reference a different layer (if not the default provided during module construction)
absolute: A boolean specifying whether the offset is absolute within the layer, or relative to the start of the module
object_type: Override for the type from the symobl to use (or if the symbol type is missing)
Returns:
The constructed object
+9 -2
View File
@@ -19,11 +19,18 @@ vollog = logging.getLogger(__name__)
try:
# TODO: Find library for windows if needed
try:
# Linux/Mac
# Linux
lib_snappy = ctypes.cdll.LoadLibrary("libsnappy.so.1")
except OSError:
lib_snappy = None
try:
if not lib_snappy:
# macOS
lib_snappy = ctypes.cdll.LoadLibrary("libsnappy.1.dylib")
except OSError:
lib_snappy = None
try:
if not lib_snappy:
# Windows 64
@@ -31,7 +38,7 @@ try:
except OSError:
lib_snappy = None
if lib_snappy:
if not lib_snappy:
# Windows 32
lib_snappy = ctypes.cdll.LoadLibrary("snappy32")
+9
View File
@@ -111,6 +111,11 @@ class Intel(linear.LinearlyMappedLayer):
"""Returns whether a particular page is valid based on its entry."""
return bool(entry & 1)
@staticmethod
def _page_is_dirty(entry: int) -> bool:
"""Returns whether a particular page is dirty based on its entry."""
return bool(entry & (1 << 6))
def canonicalize(self, addr: int) -> int:
"""Canonicalizes an address by performing an appropiate sign extension on the higher addresses"""
if self._bits_per_register <= self._maxvirtaddr:
@@ -259,6 +264,10 @@ class Intel(linear.LinearlyMappedLayer):
except exceptions.InvalidAddressException:
return False
def is_dirty(self, offset: int) -> bool:
"""Returns whether the page at offset is marked dirty"""
return self._page_is_dirty(self._translate_entry(offset)[0])
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
@@ -0,0 +1,208 @@
# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from dataclasses import dataclass, astuple, fields
from typing import Iterable, List, Tuple
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
from volatility3.framework.symbols.linux import extensions
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
@dataclass
class TaskData:
"""Stores basic information about a task"""
comm: str
pid: int
tgid: int
ppid: int
euid: int
@dataclass
class CapabilitiesData:
"""Stores each set of capabilties for a task"""
cap_inheritable: interfaces.objects.ObjectInterface
cap_permitted: interfaces.objects.ObjectInterface
cap_effective: interfaces.objects.ObjectInterface
cap_bset: interfaces.objects.ObjectInterface
cap_ambient: interfaces.objects.ObjectInterface
def astuple(self) -> Tuple:
"""Returns a shallow copy of the capability sets in a tuple.
Otherwise, when dataclasses.astuple() performs a deep-copy recursion on
ObjectInterface will take a substantial amount of time.
"""
return tuple(getattr(self, field.name) for field in fields(self))
class Capabilities(plugins.PluginInterface):
"""Lists process capabilities"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pids",
description="Filter on specific process IDs.",
element_type=int,
optional=True,
),
]
def _check_capabilities_support(
self,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
):
"""Checks that the framework supports at least as much capabilities as
the kernel being analysed. Otherwise, it shows a warning for the
developers.
"""
vmlinux = context.modules[vmlinux_module_name]
try:
kernel_cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap")
except exceptions.SymbolError:
# It should be a kernel < 3.2
return
vol2_last_cap = extensions.kernel_cap_struct.get_last_cap_value()
if kernel_cap_last_cap > vol2_last_cap:
vollog.warning(
"Developers: The supported Linux capabilities of this plugin are outdated for this kernel"
)
@staticmethod
def _decode_cap(cap: interfaces.objects.ObjectInterface) -> str:
"""Returns a textual representation of the capability set.
The format is a comma-separated list of capabilitites. In order to
summarize the output and if all the capabilities are enabled, instead of
the individual capabilities, the special name "all" will be shown.
Args:
cap: Kernel capability object. Usually a 'kernel_cap_struct' struct
Returns:
str: A string with a comma separated list of decoded capabilities
"""
if isinstance(cap, renderers.NotAvailableValue):
return cap
cap_value = cap.get_capabilities()
if not cap_value:
return ""
if cap_value == cap.get_kernel_cap_full():
return "all"
return ", ".join(cap.enumerate_capabilities())
@classmethod
def get_task_capabilities(
cls, task: interfaces.objects.ObjectInterface
) -> Tuple[TaskData, CapabilitiesData]:
"""Returns a tuple with the task basic information along with its capabilities
Args:
task: A task object from where to get the fields.
Returns:
A tuple with the task basic information and its capabilities
"""
task_data = TaskData(
comm=utility.array_to_string(task.comm),
pid=int(task.pid),
tgid=int(task.tgid),
ppid=int(task.parent.pid),
euid=int(task.cred.euid),
)
task_cred = task.real_cred
capabilities_data = CapabilitiesData(
cap_inheritable=task_cred.cap_inheritable,
cap_permitted=task_cred.cap_permitted,
cap_effective=task_cred.cap_effective,
cap_bset=task_cred.cap_bset,
cap_ambient=renderers.NotAvailableValue(),
)
# Ambient capabilities were added in kernels 4.3.6
if task_cred.has_member("cap_ambient"):
capabilities_data.cap_ambient = task_cred.cap_ambient
return task_data, capabilities_data
@classmethod
def get_tasks_capabilities(
cls, tasks: List[interfaces.objects.ObjectInterface]
) -> Iterable[Tuple[TaskData, CapabilitiesData]]:
"""Yields a tuple for each task containing the task's basic information along with its capabilities
Args:
tasks: An iterable with the tasks to process.
Yields:
A tuple for each task containing the task's basic information and its capabilities
"""
for task in tasks:
yield cls.get_task_capabilities(task)
def _generator(
self, tasks: Iterable[interfaces.objects.ObjectInterface]
) -> Iterable[Tuple[int, Tuple]]:
for task_fields, capabilities_fields in self.get_tasks_capabilities(tasks):
task_fields = astuple(task_fields)
capabilities_text = tuple(
self._decode_cap(cap) for cap in capabilities_fields.astuple()
)
yield 0, task_fields + capabilities_text
def run(self):
self._check_capabilities_support(self.context, self.config["kernel"])
pids = self.config.get("pids")
pid_filter = pslist.PsList.create_pid_filter(pids)
tasks = pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=pid_filter
)
columns = [
("Name", str),
("Tid", int),
("Pid", int),
("PPid", int),
("EUID", int),
("cap_inheritable", str),
("cap_permitted", str),
("cap_effective", str),
("cap_bounding", str),
("cap_ambient", str),
]
return renderers.TreeGrid(columns, self._generator(tasks))
+109 -2
View File
@@ -4,20 +4,26 @@
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
from typing import List
import logging
from typing import List, Optional, Type
from volatility3.framework import renderers, interfaces
from volatility3.framework import constants, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class Elfs(plugins.PluginInterface):
"""Lists all memory mapped ELF files for all processes."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -36,9 +42,93 @@ class Elfs(plugins.PluginInterface):
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed processes",
default=False,
optional=True,
),
]
@classmethod
def elf_dump(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
elf_table_name: str,
vma: interfaces.objects.ObjectInterface,
task: interfaces.objects.ObjectInterface,
open_method: Type[interfaces.plugins.FileHandlerInterface],
) -> Optional[interfaces.plugins.FileHandlerInterface]:
"""Extracts an ELF as a FileHandlerInterface
Args:
context: the context to operate upon
layer_name: The name of the layer on which to operate
elf_table_name: the name for the symbol table containing the symbols for ELF-files
vma: virtual memory allocation of ELF
task: the task object whose memory should be output
open_method: class to provide context manager for opening the file
Returns:
An open FileHandlerInterface object containing the complete data for the task or None in the case of failure
"""
proc_layer = context.layers[layer_name]
file_handle = None
elf_object = context.object(
elf_table_name + constants.BANG + "Elf",
offset=vma.vm_start,
layer_name=layer_name,
)
if not elf_object.is_valid():
return None
sections = {}
# TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ?
for phdr in elf_object.get_program_headers():
if phdr.p_type != 1: # PT_LOAD = 1
continue
start = phdr.p_vaddr
size = phdr.p_memsz
end = start + size
# Use complete memory pages for dumping
# If start isn't a multiple of 4096, stick to the highest multiple < start
# If end isn't a multiple of 4096, stick to the lowest multiple > end
if start % 4096:
start = start & ~0xFFF
if end % 4096:
end = (end & ~0xFFF) + 4096
real_size = end - start
# Check if ELF has a legitimate size
if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE:
raise ValueError(f"The claimed size of the ELF is invalid: {real_size}")
sections[start] = real_size
elf_data = b""
for section_start in sorted(sections.keys()):
read_size = sections[section_start]
buf = proc_layer.read(vma.vm_start + section_start, read_size, pad=True)
elf_data = elf_data + buf
file_handle = open_method(
f"pid.{task.pid}.{utility.array_to_string(task.comm)}.{vma.vm_start:#x}.dmp"
)
file_handle.write(elf_data)
return file_handle
def _generator(self, tasks):
elf_table_name = intermed.IntermediateSymbolTable.create(
self.context, self.config_path, "linux", "elf", class_types=elf.class_types
)
for task in tasks:
proc_layer_name = task.add_process_layer()
if not proc_layer_name:
@@ -60,6 +150,21 @@ class Elfs(plugins.PluginInterface):
path = vma.get_name(self.context, task)
file_output = "Disabled"
if self.config["dump"]:
file_handle = self.elf_dump(
self.context,
proc_layer_name,
elf_table_name,
vma,
task,
self.open,
)
file_output = "Error outputting file"
if file_handle:
file_handle.close()
file_output = str(file_handle.preferred_filename)
yield (
0,
(
@@ -68,6 +173,7 @@ class Elfs(plugins.PluginInterface):
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
path,
file_output,
),
)
@@ -81,6 +187,7 @@ class Elfs(plugins.PluginInterface):
("Start", format_hints.Hex),
("End", format_hints.Hex),
("File Path", str),
("File Output", str),
],
self._generator(
pslist.PsList.list_tasks(
+11 -2
View File
@@ -3,7 +3,7 @@
#
from typing import List
import logging
from volatility3.framework import constants, interfaces
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
@@ -11,6 +11,8 @@ from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class Malfind(interfaces.plugins.PluginInterface):
"""Lists process memory ranges that potentially contain injected code."""
@@ -47,7 +49,14 @@ class Malfind(interfaces.plugins.PluginInterface):
proc_layer = self.context.layers[proc_layer_name]
for vma in task.mm.get_vma_iter():
if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]":
vma_name = vma.get_name(self.context, task)
vollog.debug(
f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}"
)
if (
vma.is_suspicious(proc_layer)
and vma.get_name(self.context, task) != "[vdso]"
):
data = proc_layer.read(vma.vm_start, 64, pad=True)
yield vma, data
+171 -4
View File
@@ -4,18 +4,25 @@
"""A module containing a collection of plugins that produce data typically
found in Linux's /proc file system."""
from volatility3.framework import renderers
import logging
from typing import Callable, Generator, Type, Optional
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class Maps(plugins.PluginInterface):
"""Lists all memory maps for all processes."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
@classmethod
def get_requirements(cls):
@@ -35,16 +42,149 @@ class Maps(plugins.PluginInterface):
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed memory segments",
default=False,
optional=True,
),
requirements.ListRequirement(
name="address",
description="Process virtual memory addresses to include "
"(all other VMA sections are excluded). This can be any "
"virtual address within the VMA section.",
element_type=int,
optional=True,
),
requirements.IntRequirement(
name="maxsize",
description="Maximum size for dumped VMA sections "
"(all the bigger sections will be ignored)",
default=cls.MAXSIZE_DEFAULT,
optional=True,
),
]
@classmethod
def list_vmas(
cls,
task: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: True,
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Memory Areas of a specific process.
Args:
task: task object from which to list the vma
filter_func: Function to take a vma and return False if it should be filtered out
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:
vollog.debug(
f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread."
)
@classmethod
def vma_dump(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
vm_start: int,
vm_end: int,
open_method: Type[interfaces.plugins.FileHandlerInterface],
maxsize: int = MAXSIZE_DEFAULT,
) -> Optional[interfaces.plugins.FileHandlerInterface]:
"""Extracts the complete data for VMA as a FileInterface.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
task: an task_struct instance
vm_start: The start virtual address from the vma to dump
vm_end: The end virtual address from the vma to dump
open_method: class to provide context manager for opening the file
maxsize: Max size of VMA section (default MAXSIZE_DEFAULT)
Returns:
An open FileInterface object containing the complete data for the task or None in the case of failure
"""
pid = task.pid
try:
proc_layer_name = task.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
pid, excp.invalid_address, excp.layer_name
)
)
return None
vm_size = vm_end - vm_start
# check if vm_size is negative, this should never happen.
if vm_size < 0:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative."
)
return None
# check if vm_size is larger than the maxsize limit, and therefore is not saved out.
if maxsize <= vm_size:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}"
)
return None
proc_layer = context.layers[proc_layer_name]
file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp"
try:
file_handle = open_method(file_name)
chunk_size = 1024 * 1024 * 10
offset = vm_start
while offset < vm_start + vm_size:
to_read = min(chunk_size, vm_start + vm_size - offset)
data = proc_layer.read(offset, to_read, pad=True)
file_handle.write(data)
offset += to_read
except Exception as excp:
vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}")
return None
return file_handle
def _generator(self, tasks):
# build filter for addresses if required
address_list = self.config.get("address", None)
if not address_list:
# do not filter as no address_list was supplied
vma_filter_func = lambda _: True
else:
# filter for any vm_start that matches the supplied address config
def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool:
addrs_in_vma = [
addr for addr in address_list if x.vm_start <= addr <= x.vm_end
]
# if any of the user supplied addresses would fall within this vma return true
if addrs_in_vma:
return True
else:
return False
vma_filter_func = vma_filter_function
for task in tasks:
if not task.mm:
continue
name = utility.array_to_string(task.comm)
for vma in task.mm.get_vma_iter():
for vma in self.list_vmas(task, filter_func=vma_filter_func):
flags = vma.get_protection()
page_offset = vma.get_page_offset()
major = 0
@@ -58,9 +198,34 @@ class Maps(plugins.PluginInterface):
major = inode_object.i_sb.major
minor = inode_object.i_sb.minor
inode = inode_object.i_ino
path = vma.get_name(self.context, task)
file_output = "Disabled"
if self.config["dump"]:
file_output = "Error outputting file"
try:
vm_start = vma.vm_start
vm_end = vma.vm_end
except AttributeError:
vollog.debug(
f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {task.pid}"
)
vm_start = None
vm_end = None
if vm_start and vm_end:
# only attempt to dump the memory if we have vm_start and vm_end
file_handle = self.vma_dump(
self.context,
task,
vm_start,
vm_end,
self.open,
self.config["maxsize"],
)
if file_handle:
file_handle.close()
file_output = file_handle.preferred_filename
yield (
0,
(
@@ -74,6 +239,7 @@ class Maps(plugins.PluginInterface):
minor,
inode,
path,
file_output,
),
)
@@ -92,6 +258,7 @@ class Maps(plugins.PluginInterface):
("Minor", int),
("Inode", int),
("File Path", str),
("File output", str),
],
self._generator(
pslist.PsList.list_tasks(
+69 -32
View File
@@ -1,12 +1,15 @@
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Callable, Iterable, List, Any, Tuple
from typing import Any, Callable, Iterable, List
from volatility3.framework import renderers, interfaces
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.plugins.linux import elfs
class PsList(interfaces.plugins.PluginInterface):
@@ -24,6 +27,9 @@ class PsList(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="elfs", plugin=elfs.Elfs, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
@@ -42,6 +48,12 @@ class PsList(interfaces.plugins.PluginInterface):
optional=True,
default=False,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed processes",
optional=True,
default=False,
),
]
@classmethod
@@ -66,38 +78,12 @@ class PsList(interfaces.plugins.PluginInterface):
else:
return lambda _: False
def _get_task_fields(
self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, str]:
"""Extract the fields needed for the final output
Args:
task: A task object from where to get the fields.
decorate_comm: If True, it decorates the comm string of
- User threads: in curly brackets,
- Kernel threads: in square brackets
Defaults to False.
Returns:
A tuple with the fields to show in the plugin output.
"""
pid = task.tgid
tid = task.pid
ppid = task.parent.tgid if task.parent else 0
name = utility.array_to_string(task.comm)
if decorate_comm:
if task.is_kernel_thread:
name = f"[{name}]"
elif task.is_user_thread:
name = f"{{{name}}}"
task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name)
return task_fields
def _generator(
self,
pid_filter: Callable[[Any], bool],
include_threads: bool = False,
decorate_comm: bool = False,
dump: bool = False,
):
"""Generates the tasks list.
@@ -110,14 +96,63 @@ class PsList(interfaces.plugins.PluginInterface):
- User threads: in curly brackets,
- Kernel threads: in square brackets
Defaults to False.
dump: If True, the main executable of the process is written to a file
Defaults to False.
Yields:
Each rows
"""
for task in self.list_tasks(
self.context, self.config["kernel"], pid_filter, include_threads
):
row = self._get_task_fields(task, decorate_comm)
yield (0, row)
elf_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"linux",
"elf",
class_types=elf.class_types,
)
file_output = "Disabled"
if dump:
proc_layer_name = task.add_process_layer()
if not proc_layer_name:
continue
# Find the vma that belongs to the main ELF of the process
file_output = "Error outputting file"
for v in task.mm.get_mmap_iter():
if v.vm_start == task.mm.start_code:
file_handle = elfs.Elfs.elf_dump(
self.context,
proc_layer_name,
elf_table_name,
v,
task,
self.open,
)
if file_handle:
file_output = str(file_handle.preferred_filename)
file_handle.close()
break
pid = task.tgid
tid = task.pid
ppid = task.parent.tgid if task.parent else 0
name = utility.array_to_string(task.comm)
if decorate_comm:
if task.is_kernel_thread:
name = f"[{name}]"
elif task.is_user_thread:
name = f"{{{name}}}"
yield 0, (
format_hints.Hex(task.vol.offset),
pid,
tid,
ppid,
name,
file_output,
)
@classmethod
def list_tasks(
@@ -155,6 +190,7 @@ class PsList(interfaces.plugins.PluginInterface):
pids = self.config.get("pid")
include_threads = self.config.get("threads")
decorate_comm = self.config.get("decorate_comm")
dump = self.config.get("dump")
filter_func = self.create_pid_filter(pids)
columns = [
@@ -163,7 +199,8 @@ class PsList(interfaces.plugins.PluginInterface):
("TID", int),
("PPID", int),
("COMM", str),
("File output", str),
]
return renderers.TreeGrid(
columns, self._generator(filter_func, include_threads, decorate_comm)
columns, self._generator(filter_func, include_threads, decorate_comm, dump)
)
@@ -83,7 +83,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
sock: Kernel generic `sock` object
Returns a tuple with:
sock: The respective kernel's *_sock object for that socket family
sock: The respective kernel's \*_sock object for that socket family
sock_stat: A tuple with the source and destination (address and port) along with its state string
socket_filter: A dictionary with information about the socket filter
"""
@@ -501,8 +501,7 @@ class Sockstat(plugins.PluginInterface):
family: Socket family string (AF_UNIX, AF_INET, etc)
sock_type: Socket type string (STREAM, DGRAM, etc)
protocol: Protocol string (UDP, TCP, etc)
sock_fields: A tuple with the *_sock object, the sock stats and the
extended info dictionary
sock_fields: A tuple with the \*_sock object, the sock stats and the extended info dictionary
"""
vmlinux = context.modules[symbol_table]
+27 -6
View File
@@ -2,12 +2,14 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import datetime
import logging
from typing import Callable, Iterable, List, Dict
from typing import Callable, Dict, Iterable, List
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import mac
vollog = logging.getLogger(__name__)
@@ -105,10 +107,20 @@ class PsList(interfaces.plugins.PluginInterface):
self.config["kernel"],
filter_func=self.create_pid_filter(self.config.get("pid", None)),
):
pid = task.p_pid
ppid = task.p_ppid
offset = format_hints.Hex(task.vol.offset)
name = utility.array_to_string(task.p_comm)
yield (0, (pid, ppid, name))
pid = task.p_pid
uid = task.p_uid
gid = task.p_gid
start_time_seconds = task.p_start.tv_sec
start_time_microseconds = task.p_start.tv_usec
start_time = datetime.datetime.fromtimestamp(
start_time_seconds + start_time_microseconds / 1e6
)
ppid = task.p_ppid
yield (0, (offset, name, pid, uid, gid, start_time, ppid))
@classmethod
def list_tasks_allproc(
@@ -310,5 +322,14 @@ class PsList(interfaces.plugins.PluginInterface):
def run(self):
return renderers.TreeGrid(
[("PID", int), ("PPID", int), ("COMM", str)], self._generator()
[
("OFFSET", format_hints.Hex),
("NAME", str),
("PID", int),
("UID", int),
("GID", int),
("Start Time", datetime.datetime),
("PPID", int),
],
self._generator(),
)
@@ -149,9 +149,9 @@ class Strings(interfaces.plugins.PluginInterface):
for mapval in layer.mapping(0x0, layer.maximum_address, ignore_errors=True):
offset, _, mapped_offset, mapped_size, maplayer = mapval
for val in range(mapped_offset, mapped_offset + mapped_size, 0x1000):
cur_set = reverse_map.get(mapped_offset >> 12, set())
cur_set = reverse_map.get(val >> 12, set())
cur_set.add(("kernel", offset))
reverse_map[mapped_offset >> 12] = cur_set
reverse_map[val >> 12] = cur_set
if progress_callback:
progress_callback(
(offset * 100) / layer.maximum_address,
@@ -198,6 +198,7 @@ class VadInfo(interfaces.plugins.PluginInterface):
def _generator(self, procs):
kernel = self.context.modules[self.config["kernel"]]
kernel_layer = self.context.layers[kernel.layer_name]
def passthrough(_: interfaces.objects.ObjectInterface) -> bool:
return False
@@ -229,7 +230,7 @@ class VadInfo(interfaces.plugins.PluginInterface):
(
proc.UniqueProcessId,
process_name,
format_hints.Hex(vad.vol.offset),
format_hints.Hex(kernel_layer.canonicalize(vad.vol.offset)),
format_hints.Hex(vad.get_start()),
format_hints.Hex(vad.get_end()),
vad.get_tag(),
+5 -1
View File
@@ -10,7 +10,7 @@ import collections
import collections.abc
import datetime
import logging
from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, Union
from volatility3.framework import interfaces
from volatility3.framework.interfaces import renderers
@@ -96,6 +96,10 @@ class TreeNode(interfaces.renderers.TreeNode):
# if isinstance(val, datetime.datetime):
# tznaive = val.tzinfo is None or val.tzinfo.utcoffset(val) is None
def asdict(self) -> Dict[str, Any]:
"""Returns the contents of the node as a dictionary"""
return self._values._asdict()
@property
def values(self) -> List[interfaces.renderers.BaseTypes]:
"""Returns the list of values from the particular node, based on column
@@ -28,6 +28,8 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("fs_struct", extensions.fs_struct)
self.set_type_class("files_struct", extensions.files_struct)
self.set_type_class("kobject", extensions.kobject)
self.set_type_class("cred", extensions.cred)
self.set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
# Might not exist in the current symbols
self.optional_set_type_class("module", extensions.module)
self.optional_set_type_class("bpf_prog", extensions.bpf_prog)
@@ -200,8 +202,11 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
Returns:
str: A file (or sock pipe) pathname relative to the task's root directory.
"""
# Memory smear protection: Check that both the file and dentry pointers are valid.
try:
dentry = filp.get_dentry()
dentry.is_root()
except exceptions.InvalidAddressException:
return ""
@@ -5,7 +5,7 @@
import collections.abc
import logging
import socket as socket_module
from typing import Generator, Iterable, Iterator, Optional, Tuple
from typing import Generator, Iterable, Iterator, Optional, Tuple, List
from volatility3.framework import constants
from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY
@@ -13,6 +13,7 @@ from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS
from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS
from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES
from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES
from volatility3.framework.constants.linux import CAPABILITIES
from volatility3.framework import exceptions, objects, interfaces, symbols
from volatility3.framework.layers import linear
from volatility3.framework.objects import utility
@@ -202,7 +203,7 @@ class task_struct(generic.GenericIntelProcess):
) -> Generator[Tuple[int, int], None, None]:
"""Returns a list of sections based on the memory manager's view of
this task's virtual memory."""
for vma in self.mm.get_mmap_iter():
for vma in self.mm.get_vma_iter():
start = int(vma.vm_start)
end = int(vma.vm_end)
@@ -577,7 +578,7 @@ class vm_area_struct(objects.StructType):
return fname
# used by malfind
def is_suspicious(self):
def is_suspicious(self, proclayer=None):
ret = False
flags_str = self.get_protection()
@@ -586,6 +587,24 @@ class vm_area_struct(objects.StructType):
ret = True
elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0:
ret = True
elif proclayer and "x" in flags_str:
for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT):
try:
if proclayer.is_dirty(i):
vollog.warning(
f"Found malicious (dirty+exec) page at {hex(i)} !"
)
# We do not attempt to find other dirty+exec pages once we have found one
ret = True
break
except (
exceptions.PagedInvalidAddressException,
exceptions.InvalidAddressException,
) as excp:
vollog.debug(f"Unable to translate address {hex(i)} : {excp}")
# Abort as it is likely that other addresses in the same range will also fail
ret = False
break
return ret
@@ -794,7 +813,7 @@ class mount(objects.StructType):
"""Gets the fs where we are mounted on
Returns:
A 'mount *'
A mount pointer
"""
return self.mnt_parent
@@ -802,7 +821,7 @@ class mount(objects.StructType):
"""Gets the dentry of the mountpoint
Returns:
A 'dentry *'
A dentry pointer
"""
return self.mnt_mountpoint
@@ -839,7 +858,7 @@ class mount(objects.StructType):
"""Returns the root of the mounted tree
Returns:
A 'dentry *'
A dentry pointer
"""
vfsmnt = self.get_vfsmnt_current()
dentry = vfsmnt.mnt_root
@@ -850,7 +869,7 @@ class mount(objects.StructType):
"""Returns the parent root of the mounted tree
Returns:
A 'dentry *'
A dentry pointer
"""
return self.get_mnt_parent().get_dentry_current()
@@ -970,17 +989,17 @@ class vfsmount(objects.StructType):
"""Helper to make sure it is comparing two pointers to 'vfsmount'.
Depending on the kernel version, the calling object (self) could be
a 'vfsmount *' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
a 'vfsmount \*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
in the framework "auto" dereferencing ability to assure that when we
reach this point 'self' will be a 'vfsmount' already and self.vol.offset
a 'vfsmount *' and not a 'vfsmount **'. The argument must be a 'vfsmount *'.
a 'vfsmount \*' and not a 'vfsmount \*\*'. The argument must be a 'vfsmount \*'.
Typically, it's called from do_get_path().
Args:
vfsmount_ptr (vfsmount *): A pointer to a 'vfsmount'
vfsmount_ptr (vfsmount \*): A pointer to a 'vfsmount'
Raises:
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount *'
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \*'
Returns:
bool: 'True' if the given argument points to the the same 'vfsmount'
@@ -1010,7 +1029,7 @@ class vfsmount(objects.StructType):
"""Returns the current fs where we are mounted on
Returns:
A 'vfsmount *'
A vfsmount pointer
"""
return self.get_mnt_parent()
@@ -1018,8 +1037,8 @@ class vfsmount(objects.StructType):
"""Gets the parent fs (vfsmount) to where it's mounted on
Returns:
For kernels < 3.3.8: A 'vfsmount *'
For kernels >= 3.3.8: A 'vfsmount'
For kernels < 3.3.8: A vfsmount pointer
For kernels >= 3.3.8: A vfsmount object
"""
if self._is_kernel_prior_to_struct_mount():
return self.get_mnt_parent()
@@ -1030,7 +1049,7 @@ class vfsmount(objects.StructType):
"""Returns the root of the mounted tree
Returns:
A 'dentry *'
A dentry pointer
"""
if self._is_kernel_prior_to_struct_mount():
return self.get_mnt_mountpoint()
@@ -1041,7 +1060,7 @@ class vfsmount(objects.StructType):
"""Returns the parent root of the mounted tree
Returns:
A 'dentry *'
A dentry pointer
"""
if self._is_kernel_prior_to_struct_mount():
return self.get_mnt_mountpoint()
@@ -1052,8 +1071,8 @@ class vfsmount(objects.StructType):
"""Gets the mnt_parent member.
Returns:
For kernels < 3.3.8: A 'vfsmount *'
For kernels >= 3.3.8: A 'mount *'
For kernels < 3.3.8: A vfsmount pointer
For kernels >= 3.3.8: A mount pointer
"""
if self._is_kernel_prior_to_struct_mount():
return self.mnt_parent
@@ -1064,7 +1083,7 @@ class vfsmount(objects.StructType):
"""Gets the dentry of the mountpoint
Returns:
A 'dentry *'
A dentry pointer
"""
if self.has_member("mnt_mountpoint"):
return self.mnt_mountpoint
@@ -1428,3 +1447,137 @@ class bpf_prog(objects.StructType):
# kernel < 3.18.140
raise AttributeError("Unable to find the BPF type")
class cred(objects.StructType):
# struct cred was added in kernels 2.6.29
def _get_cred_int_value(self, member: str) -> int:
"""Helper to obtain the right cred member value for the current kernel.
Args:
member (str): The requested cred member name to obtain its value
Raises:
AttributeError: When the requested cred member doesn't exist
AttributeError: When the cred implementation is not supported.
Returns:
int: The cred member value
"""
if not self.has_member(member):
raise AttributeError(f"struct cred doesn't have a '{member}' member")
cred_val = self.member(member)
if hasattr(cred_val, "val"):
# From kernels 3.5.7 on it is a 'kuid_t' type
value = cred_val.val
elif isinstance(cred_val, objects.Integer):
# From at least 2.6.30 and until 3.5.7 it was a 'uid_t' type which was an 'unsigned int'
value = cred_val
else:
raise AttributeError("Kernel struct cred is not supported")
return int(value)
@property
def euid(self):
"""Returns the effective user ID
Returns:
int: the effective user ID value
"""
return self._get_cred_int_value("euid")
class kernel_cap_struct(objects.StructType):
# struct kernel_cap_struct was added in kernels 2.5.0
@classmethod
def get_last_cap_value(cls) -> int:
"""Returns the latest capability ID supported by the framework.
Returns:
int: The latest supported capability ID supported by the framework.
"""
return len(CAPABILITIES) - 1
def get_kernel_cap_full(self) -> int:
"""Return the maximum value allowed for this kernel for a capability
Returns:
int: _description_
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
try:
cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap")
except exceptions.SymbolError:
# It should be a kernel < 3.2, let's use our list of capabilities
cap_last_cap = self.get_last_cap_value()
return (1 << cap_last_cap + 1) - 1
@classmethod
def capabilities_to_string(cls, capabilities_bitfield: int) -> List[str]:
"""Translates a capability bitfield to a list of capability strings.
Args:
capabilities_bitfield (int): The capability bitfield value.
Returns:
List[str]: A list of capability strings.
"""
capabilities = []
for bit, name in enumerate(CAPABILITIES):
if capabilities_bitfield & (1 << bit) != 0:
capabilities.append(name)
return capabilities
def get_capabilities(self) -> int:
"""Returns the capability bitfield value
Returns:
int: The capability bitfield value.
"""
if isinstance(self.cap, objects.Array):
# In 2.6.25.x <= kernels < 6.3 kernel_cap_struct::cap is a two
# elements __u32 array that constitutes a 64bit bitfield.
# Technically, it can also be an array of 1 element if
# _KERNEL_CAPABILITY_U32S = _LINUX_CAPABILITY_U32S_1
# However, in the source code, that never happens.
# From 2.6.24 to 2.6.25 cap became an array of 2 elements.
cap_value = (self.cap[1] << 32) | self.cap[0]
else:
# In kernels < 2.6.25.x kernel_cap_struct::cap was a __u32
# In kernels >= 6.3 kernel_cap_struct::cap is a u64
cap_value = self.cap
return cap_value & self.get_kernel_cap_full()
def enumerate_capabilities(self) -> List[str]:
"""Returns the list of capability strings.
Returns:
List[str]: The list of capability strings.
"""
capabilities_value = self.get_capabilities()
return self.capabilities_to_string(capabilities_value)
def has_capability(self, capability: str) -> bool:
"""Checks if the given capability string is enabled.
Args:
capability (str): A string representing the capability i.e. dac_read_search
Raises:
AttributeError: If the given capability is unknown to the framework.
Returns:
bool: "True" if the given capability is enabled.
"""
if capability not in CAPABILITIES:
raise AttributeError(f"Unknown capability with name '{capability}'")
cap_value = 1 << CAPABILITIES.index(capability)
return cap_value & self.get_capabilities() != 0