mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-09 19:27:39 +02:00
Merge branch 'volatilityfoundation:develop' into vmayarascan
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
name: Install Volatility3 test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
install_test:
|
||||
runs-on: ${{ matrix.host }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
host: [ ubuntu-latest, windows-latest ]
|
||||
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup python-pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install volatility3
|
||||
run: pip install .
|
||||
|
||||
- name: Run volatility3
|
||||
run: vol --help
|
||||
@@ -0,0 +1,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,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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user