mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-08 18:57:38 +02:00
Merge branch 'volatilityfoundation:develop' into mac_pslist
This commit is contained in:
+5
-1
@@ -12,8 +12,12 @@ sphinx:
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
formats: all
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: doc/requirements.txt
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# These packages are required for building the documentation.
|
||||
sphinx>=4.0.0
|
||||
sphinx>=4.0.0,<7
|
||||
sphinx_autodoc_typehints>=1.4.0
|
||||
sphinx-rtd-theme>=0.4.3
|
||||
|
||||
yara-python
|
||||
pycryptodome
|
||||
pefile
|
||||
|
||||
+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(
|
||||
argv=["-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"
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
macOS Tutorial
|
||||
==============
|
||||
|
||||
This guide will give you a brief overview of how volatility3 works as well as a demonstration of several of the plugins available in the suite.
|
||||
|
||||
Acquiring memory
|
||||
----------------
|
||||
|
||||
Volatility3 does not provide the ability to acquire memory. The example below is an open source tool. Other commercial tools are also available.
|
||||
|
||||
* `osxpmem <https://github.com/Velocidex/c-aff4/releases/download/3.2/osxpmem_3.2.zip>`_
|
||||
|
||||
|
||||
|
||||
Procedure to create symbol tables for macOS
|
||||
--------------------------------------------
|
||||
|
||||
To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`.
|
||||
|
||||
.. tip:: It may be possible to locate pre-made ISF files from the `download link <https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>`_ ,
|
||||
which is built and maintained by `volatilityfoundation <https://www.volatilityfoundation.org/>`_.
|
||||
After creating the file or downloading it from the link, place the file under the directory ``volatility3/symbols/``.
|
||||
|
||||
|
||||
Listing plugins
|
||||
---------------
|
||||
|
||||
The following is a sample of the macOS plugins available for volatility3, it is not complete and more plugins may
|
||||
be added. For a complete reference, please see the volatility 3 :doc:`list of plugins <volatility3.plugins>`.
|
||||
For plugin requests, please create an issue with a description of the requested plugin.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py --help | grep -i mac. | head -n 4
|
||||
mac.bash.Bash Recovers bash command history from memory.
|
||||
mac.check_syscall.Check_syscall
|
||||
mac.check_sysctl.Check_sysctl
|
||||
mac.check_trap_table.Check_trap_table
|
||||
|
||||
.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of macOS plugins.
|
||||
|
||||
|
||||
Using plugins
|
||||
-------------
|
||||
|
||||
The following is the syntax to run the volatility CLI.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f <path to memory image> <plugin_name> <plugin_option>
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
banners
|
||||
~~~~~~~
|
||||
|
||||
In this example we will be using a memory dump from the Securinets CTF Quals 2019 Challenge called Contact_me. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge.
|
||||
Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/securinets-ctf/2019/08/24/SecurinetsQuals2019-Contact-Me.html>`_.
|
||||
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me banners.Banners
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
|
||||
Progress: 100.00 PDB scanning finished
|
||||
Offset Banner
|
||||
|
||||
0x4d2c7d0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0xb42b180 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0xcda9100 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x1275e7d0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x1284fba4 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x34ad0180 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
|
||||
|
||||
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-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
|
||||
|
||||
$ python3 vol.py -f contact_me mac.pslist.PsList
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
|
||||
PID PPID COMM
|
||||
|
||||
0 0 kernel_task
|
||||
1 0 launchd
|
||||
35 1 UserEventAgent
|
||||
38 1 kextd
|
||||
39 1 fseventsd
|
||||
37 1 uninstalld
|
||||
45 1 configd
|
||||
46 1 powerd
|
||||
52 1 logd
|
||||
58 1 warmd
|
||||
.....
|
||||
|
||||
``mac.pslist`` helps us to list the processes which are running, their PIDs and PPIDs.
|
||||
|
||||
mac.pstree
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me mac.pstree.PsTree
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
PID PPID COMM
|
||||
|
||||
35 1 UserEventAgent
|
||||
38 1 kextd
|
||||
39 1 fseventsd
|
||||
37 1 uninstalld
|
||||
204 1 softwareupdated
|
||||
* 449 204 SoftwareUpdateCo
|
||||
337 1 system_installd
|
||||
* 455 337 update_dyld_shar
|
||||
|
||||
``mac.pstree`` helps us to display the parent child relationships between processes.
|
||||
|
||||
mac.ifconfig
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me mac.ifconfig.Ifconfig
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
Interface IP Address Mac Address Promiscuous
|
||||
|
||||
lo0 False
|
||||
lo0 127.0.0.1 False
|
||||
lo0 ::1 False
|
||||
lo0 fe80:1::1 False
|
||||
gif0 False
|
||||
stf0 False
|
||||
en0 00:0C:29:89:8B:F0 00:0C:29:89:8B:F0 False
|
||||
en0 fe80:4::10fb:c89d:217f:52ae 00:0C:29:89:8B:F0 False
|
||||
en0 192.168.140.128 00:0C:29:89:8B:F0 False
|
||||
utun0 False
|
||||
utun0 fe80:5::2a95:bb15:87e3:977c False
|
||||
|
||||
we can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation.
|
||||
@@ -25,6 +25,7 @@ There is also some information to get you started quickly:
|
||||
:caption: Getting Started
|
||||
|
||||
getting-started-linux-tutorial
|
||||
getting-started-mac-tutorial
|
||||
getting-started-windows-tutorial
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
CAP_FULL = 0xFFFFFFFF
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# 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.constants.linux import CAP_FULL
|
||||
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_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))
|
||||
@@ -48,7 +48,7 @@ class Elfs(plugins.PluginInterface):
|
||||
|
||||
name = utility.array_to_string(task.comm)
|
||||
|
||||
for vma in task.mm.get_mmap_iter():
|
||||
for vma in task.mm.get_vma_iter():
|
||||
hdr = proc_layer.read(vma.vm_start, 4, pad=True)
|
||||
if not (
|
||||
hdr[0] == 0x7F
|
||||
|
||||
@@ -66,7 +66,7 @@ class IOMem(interfaces.plugins.PluginInterface):
|
||||
name = utility.pointer_to_string(resource.name, 128)
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.warning(
|
||||
"Unable to follow pointer to name for resource object at {resource_offset:#x}, "
|
||||
f"Unable to follow pointer to name for resource object at {resource_offset:#x}, "
|
||||
"replaced with UnreadableValue"
|
||||
)
|
||||
name = renderers.UnreadableValue()
|
||||
|
||||
@@ -46,7 +46,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
|
||||
for vma in task.mm.get_mmap_iter():
|
||||
for vma in task.mm.get_vma_iter():
|
||||
if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]":
|
||||
data = proc_layer.read(vma.vm_start, 64, pad=True)
|
||||
yield vma, data
|
||||
|
||||
@@ -9,8 +9,10 @@ from typing import Tuple, List, Iterable, Union
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.symbols import linux
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
MountInfoData = namedtuple(
|
||||
@@ -48,6 +50,9 @@ class MountInfo(plugins.PluginInterface):
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pids",
|
||||
description="Filter on specific process IDs.",
|
||||
@@ -71,37 +76,6 @@ class MountInfo(plugins.PluginInterface):
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _do_get_path(cls, mnt, fs_root) -> Union[None, str]:
|
||||
"""It mimics the Linux kernel prepend_path function."""
|
||||
vfsmnt = mnt.mnt
|
||||
dentry = vfsmnt.get_mnt_root()
|
||||
|
||||
path_reversed = []
|
||||
while dentry != fs_root.dentry or vfsmnt.vol.offset != fs_root.mnt:
|
||||
if dentry == vfsmnt.get_mnt_root() or dentry.is_root():
|
||||
parent = mnt.get_mnt_parent().dereference()
|
||||
# Escaped?
|
||||
if dentry != vfsmnt.get_mnt_root():
|
||||
return None
|
||||
|
||||
# Global root?
|
||||
if mnt.vol.offset != parent.vol.offset:
|
||||
dentry = mnt.get_mnt_mountpoint()
|
||||
mnt = parent
|
||||
vfsmnt = mnt.mnt
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
parent = dentry.d_parent
|
||||
dname = dentry.d_name.name_as_str()
|
||||
path_reversed.append(dname.strip("/"))
|
||||
dentry = parent
|
||||
|
||||
path = "/" + "/".join(reversed(path_reversed))
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def get_mountinfo(
|
||||
cls, mnt, task
|
||||
@@ -115,8 +89,8 @@ class MountInfo(plugins.PluginInterface):
|
||||
if not mnt_root:
|
||||
return None
|
||||
|
||||
path_root = cls._do_get_path(mnt, task.fs.root)
|
||||
if path_root is None:
|
||||
path_root = linux.LinuxUtilities.get_path_mnt(task, mnt)
|
||||
if not path_root:
|
||||
return None
|
||||
|
||||
mnt_root_path = mnt_root.path()
|
||||
@@ -170,9 +144,11 @@ class MountInfo(plugins.PluginInterface):
|
||||
)
|
||||
|
||||
def _get_tasks_mountpoints(
|
||||
self, tasks: Iterable[interfaces.objects.ObjectInterface], per_namespace: bool
|
||||
self,
|
||||
tasks: Iterable[interfaces.objects.ObjectInterface],
|
||||
filtered_by_pids: bool,
|
||||
):
|
||||
seen_namespaces = set()
|
||||
seen_mountpoints = set()
|
||||
for task in tasks:
|
||||
if not (
|
||||
task
|
||||
@@ -181,19 +157,27 @@ class MountInfo(plugins.PluginInterface):
|
||||
and task.nsproxy
|
||||
and task.nsproxy.mnt_ns
|
||||
):
|
||||
# This task doesn't have all the information required
|
||||
# This task doesn't have all the information required.
|
||||
# It should be a kernel < 2.6.30
|
||||
continue
|
||||
|
||||
mnt_namespace = task.nsproxy.mnt_ns
|
||||
mnt_ns_id = mnt_namespace.get_inode()
|
||||
|
||||
if per_namespace:
|
||||
if mnt_ns_id in seen_namespaces:
|
||||
continue
|
||||
else:
|
||||
seen_namespaces.add(mnt_ns_id)
|
||||
try:
|
||||
mnt_ns_id = mnt_namespace.get_inode()
|
||||
except AttributeError:
|
||||
mnt_ns_id = renderers.NotAvailableValue()
|
||||
|
||||
for mount in mnt_namespace.get_mount_points():
|
||||
# When PIDs are filtered, it makes sense that the user want to
|
||||
# see each of those processes mount points. So we don't filter
|
||||
# by mount id in this case.
|
||||
if not filtered_by_pids:
|
||||
mnt_id = int(mount.mnt_id)
|
||||
if mnt_id in seen_mountpoints:
|
||||
continue
|
||||
else:
|
||||
seen_mountpoints.add(mnt_id)
|
||||
|
||||
yield task, mount, mnt_ns_id
|
||||
|
||||
def _generator(
|
||||
@@ -201,10 +185,20 @@ class MountInfo(plugins.PluginInterface):
|
||||
tasks: Iterable[interfaces.objects.ObjectInterface],
|
||||
mnt_ns_ids: List[int],
|
||||
mount_format: bool,
|
||||
per_namespace: bool,
|
||||
filtered_by_pids: bool,
|
||||
) -> Iterable[Tuple[int, Tuple]]:
|
||||
for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(tasks, per_namespace):
|
||||
if mnt_ns_ids and mnt_ns_id not in mnt_ns_ids:
|
||||
show_filter_warning = False
|
||||
for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(
|
||||
tasks, filtered_by_pids
|
||||
):
|
||||
if mnt_ns_ids and isinstance(mnt_ns_id, renderers.NotAvailableValue):
|
||||
show_filter_warning = True
|
||||
|
||||
if (
|
||||
not isinstance(mnt_ns_id, renderers.NotAvailableValue)
|
||||
and mnt_ns_ids
|
||||
and mnt_ns_id not in mnt_ns_ids
|
||||
):
|
||||
continue
|
||||
|
||||
mnt_info = self.get_mountinfo(mnt, task)
|
||||
@@ -242,12 +236,17 @@ class MountInfo(plugins.PluginInterface):
|
||||
]
|
||||
|
||||
fields_values = [mnt_ns_id]
|
||||
if not per_namespace:
|
||||
if filtered_by_pids:
|
||||
fields_values.append(task.pid)
|
||||
fields_values.extend(extra_fields_values)
|
||||
|
||||
yield (0, fields_values)
|
||||
|
||||
if show_filter_warning:
|
||||
vollog.warning(
|
||||
"Could not filter by mount namespace id. This field is not available in this kernel."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
pids = self.config.get("pids")
|
||||
mount_ns_ids = self.config.get("mntns")
|
||||
@@ -263,9 +262,9 @@ class MountInfo(plugins.PluginInterface):
|
||||
# to displays the mountpoints per namespace.
|
||||
if pids:
|
||||
columns.append(("PID", int))
|
||||
per_namespace = False
|
||||
filtered_by_pids = True
|
||||
else:
|
||||
per_namespace = True
|
||||
filtered_by_pids = False
|
||||
|
||||
if self.config.get("mount-format"):
|
||||
extra_columns = [
|
||||
@@ -292,5 +291,6 @@ class MountInfo(plugins.PluginInterface):
|
||||
columns.extend(extra_columns)
|
||||
|
||||
return renderers.TreeGrid(
|
||||
columns, self._generator(tasks, mount_ns_ids, mount_format, per_namespace)
|
||||
columns,
|
||||
self._generator(tasks, mount_ns_ids, mount_format, filtered_by_pids),
|
||||
)
|
||||
|
||||
@@ -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_mmap_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(
|
||||
|
||||
@@ -28,7 +28,11 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
self._vmlinux = vmlinux
|
||||
self._task = task
|
||||
|
||||
netns_id = task.nsproxy.net_ns.get_inode()
|
||||
try:
|
||||
netns_id = task.nsproxy.net_ns.get_inode()
|
||||
except AttributeError:
|
||||
netns_id = NotAvailableValue()
|
||||
|
||||
self._netdevices = self._build_network_devices_map(netns_id)
|
||||
|
||||
self._sock_family_handlers = {
|
||||
@@ -61,7 +65,10 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
self._vmlinux.symbol_table_name + constants.BANG + "net_device"
|
||||
)
|
||||
for net_dev in net.dev_base_head.to_list(net_device_symname, "dev_list"):
|
||||
if net.get_inode() != netns_id:
|
||||
if (
|
||||
isinstance(netns_id, NotAvailableValue)
|
||||
or net.get_inode() != netns_id
|
||||
):
|
||||
continue
|
||||
dev_name = utility.array_to_string(net_dev.name)
|
||||
netdevices_map[net_dev.ifindex] = dev_name
|
||||
@@ -76,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
|
||||
"""
|
||||
@@ -143,19 +150,32 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
return
|
||||
|
||||
bpfprog = sock_filter.prog
|
||||
if bpfprog.type == 0:
|
||||
# BPF_PROG_TYPE_UNSPEC = 0
|
||||
|
||||
BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter
|
||||
try:
|
||||
bpfprog_type = bpfprog.get_type()
|
||||
if bpfprog_type == BPF_PROG_TYPE_UNSPEC:
|
||||
return # cBPF filter
|
||||
except AttributeError:
|
||||
# kernel < 3.18.140, it's a cBPF filter
|
||||
return
|
||||
|
||||
BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter
|
||||
if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER:
|
||||
socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})"
|
||||
vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket")
|
||||
return
|
||||
|
||||
socket_filter["bpf_filter_type"] = "eBPF"
|
||||
if not bpfprog.has_member("aux") or not bpfprog.aux:
|
||||
return
|
||||
return # kernel < 3.18.140
|
||||
bpfprog_aux = bpfprog.aux
|
||||
|
||||
if bpfprog_aux.has_member("id"):
|
||||
# `id` member was added to `bpf_prog_aux` in kernels 4.13
|
||||
# `id` member was added to `bpf_prog_aux` in kernels 4.13.16
|
||||
socket_filter["bpf_filter_id"] = str(bpfprog_aux.id)
|
||||
if bpfprog_aux.has_member("name"):
|
||||
# `name` was added to `bpf_prog_aux` in kernels 4.15
|
||||
# `name` was added to `bpf_prog_aux` in kernels 4.15.18
|
||||
bpfprog_name = utility.array_to_string(bpfprog_aux.name)
|
||||
if bpfprog_name:
|
||||
socket_filter["bpf_filter_name"] = bpfprog_name
|
||||
@@ -227,14 +247,22 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
|
||||
if netlink_sock.groups:
|
||||
groups_bitmap = netlink_sock.groups.dereference()
|
||||
src_addr = f"groups:0x{groups_bitmap:08x}"
|
||||
src_port = netlink_sock.portid
|
||||
|
||||
try:
|
||||
# Kernel >= 3.7.10
|
||||
src_port = netlink_sock.get_portid()
|
||||
except AttributeError:
|
||||
src_port = NotAvailableValue()
|
||||
|
||||
dst_addr = f"group:0x{netlink_sock.dst_group:08x}"
|
||||
module = netlink_sock.module
|
||||
if module and module.name:
|
||||
module_name_str = utility.array_to_string(module.name)
|
||||
dst_addr = f"{dst_addr},lkm:{module_name_str}"
|
||||
dst_port = netlink_sock.dst_portid
|
||||
try:
|
||||
dst_port = netlink_sock.get_dst_portid()
|
||||
except AttributeError:
|
||||
dst_port = NotAvailableValue()
|
||||
|
||||
state = netlink_sock.get_state()
|
||||
|
||||
@@ -473,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]
|
||||
|
||||
@@ -518,7 +545,11 @@ class Sockstat(plugins.PluginInterface):
|
||||
protocol = child_sock.get_protocol()
|
||||
|
||||
net = task.nsproxy.net_ns
|
||||
netns_id = net.get_inode()
|
||||
try:
|
||||
netns_id = net.get_inode()
|
||||
except AttributeError:
|
||||
netns_id = NotAvailableValue()
|
||||
|
||||
yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields
|
||||
|
||||
def _format_fields(self, sock_stat, protocol):
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
from typing import Iterator, List, Tuple, Optional
|
||||
from typing import Iterator, List, Tuple, Optional, Union
|
||||
|
||||
from volatility3 import framework
|
||||
from volatility3.framework import constants, exceptions, interfaces, objects
|
||||
@@ -28,8 +28,11 @@ 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)
|
||||
|
||||
# Mount
|
||||
self.set_type_class("vfsmount", extensions.vfsmount)
|
||||
@@ -50,92 +53,113 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
self.optional_set_type_class("bt_sock", extensions.bt_sock)
|
||||
self.optional_set_type_class("xdp_sock", extensions.xdp_sock)
|
||||
|
||||
# Only found in 6.1+ kernels
|
||||
self.optional_set_type_class("maple_tree", extensions.maple_tree)
|
||||
|
||||
|
||||
class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
"""Class with multiple useful linux functions."""
|
||||
|
||||
_version = (2, 0, 0)
|
||||
_version = (2, 1, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
framework.require_interface_version(*_required_framework_version)
|
||||
|
||||
# based on __d_path from the Linux kernel
|
||||
@classmethod
|
||||
def _do_get_path(cls, rdentry, rmnt, dentry, vfsmnt) -> str:
|
||||
ret_path: List[str] = []
|
||||
def _get_path_file(cls, task, filp) -> str:
|
||||
"""Returns the file pathname relative to the task's root directory.
|
||||
|
||||
while dentry != rdentry or vfsmnt != rmnt:
|
||||
dname = dentry.path()
|
||||
if dname == "":
|
||||
break
|
||||
Args:
|
||||
task (task_struct): A reference task
|
||||
filp (file *): A pointer to an open file
|
||||
|
||||
ret_path.insert(0, dname.strip("/"))
|
||||
if dentry == vfsmnt.get_mnt_root() or dentry == dentry.d_parent:
|
||||
if vfsmnt.get_mnt_parent() == vfsmnt:
|
||||
Returns:
|
||||
str: File pathname relative to the task's root directory.
|
||||
"""
|
||||
rdentry = task.fs.get_root_dentry()
|
||||
rmnt = task.fs.get_root_mnt()
|
||||
vfsmnt = filp.get_vfsmnt()
|
||||
dentry = filp.get_dentry()
|
||||
|
||||
return cls.do_get_path(rdentry, rmnt, dentry, vfsmnt)
|
||||
|
||||
@classmethod
|
||||
def get_path_mnt(cls, task, mnt) -> str:
|
||||
"""Returns the mount point pathname relative to the task's root directory.
|
||||
|
||||
Args:
|
||||
task (task_struct): A reference task
|
||||
mnt (vfsmount or mount): A mounted filesystem or a mount point.
|
||||
- kernels < 3.3.8 type is 'vfsmount'
|
||||
- kernels >= 3.3.8 type is 'mount'
|
||||
|
||||
Returns:
|
||||
str: Pathname of the mount point relative to the task's root directory.
|
||||
"""
|
||||
rdentry = task.fs.get_root_dentry()
|
||||
rmnt = task.fs.get_root_mnt()
|
||||
|
||||
vfsmnt = mnt.get_vfsmnt_current()
|
||||
dentry = mnt.get_dentry_current()
|
||||
|
||||
return cls.do_get_path(rdentry, rmnt, dentry, vfsmnt)
|
||||
|
||||
@classmethod
|
||||
def do_get_path(cls, rdentry, rmnt, dentry, vfsmnt) -> Union[None, str]:
|
||||
"""Returns a pathname of the mount point or file
|
||||
It mimics the Linux kernel prepend_path function.
|
||||
|
||||
Args:
|
||||
rdentry (dentry *): A pointer to the root dentry
|
||||
rmnt (vfsmount *): A pointer to the root vfsmount
|
||||
dentry (dentry *): A pointer to the dentry
|
||||
vfsmnt (vfsmount *): A pointer to the vfsmount
|
||||
|
||||
Returns:
|
||||
str: Pathname of the mount point or file
|
||||
"""
|
||||
|
||||
path_reversed = []
|
||||
while dentry != rdentry or not vfsmnt.is_equal(rmnt):
|
||||
if dentry == vfsmnt.get_mnt_root() or dentry.is_root():
|
||||
# Escaped?
|
||||
if dentry != vfsmnt.get_mnt_root():
|
||||
break
|
||||
|
||||
dentry = vfsmnt.get_mnt_mountpoint()
|
||||
vfsmnt = vfsmnt.get_mnt_parent()
|
||||
# Global root?
|
||||
if not vfsmnt.has_parent():
|
||||
break
|
||||
|
||||
dentry = vfsmnt.get_dentry_parent()
|
||||
vfsmnt = vfsmnt.get_vfsmnt_parent()
|
||||
|
||||
continue
|
||||
|
||||
parent = dentry.d_parent
|
||||
dname = dentry.d_name.name_as_str()
|
||||
path_reversed.append(dname.strip("/"))
|
||||
dentry = parent
|
||||
|
||||
# if we did not gather any valid dentrys in the path, then the entire file is
|
||||
# either 1) smeared out of memory or 2) de-allocated and corresponding structures overwritten
|
||||
# we return an empty string in this case to avoid confusion with something like a handle to the root
|
||||
# directory (e.g., "/")
|
||||
if not ret_path:
|
||||
return ""
|
||||
|
||||
ret_val = "/".join([str(p) for p in ret_path if p != ""])
|
||||
|
||||
if ret_val.startswith(("socket:", "pipe:")):
|
||||
if ret_val.find("]") == -1:
|
||||
try:
|
||||
inode = dentry.d_inode
|
||||
ino = inode.i_ino
|
||||
except exceptions.InvalidAddressException:
|
||||
ino = 0
|
||||
|
||||
ret_val = ret_val[:-1] + f":[{ino}]"
|
||||
else:
|
||||
ret_val = ret_val.replace("/", "")
|
||||
|
||||
elif ret_val != "inotify":
|
||||
ret_val = "/" + ret_val
|
||||
|
||||
return ret_val
|
||||
|
||||
# method used by 'older' kernels
|
||||
# TODO: lookup when dentry_operations->d_name was merged into the mainline kernel for exact version
|
||||
@classmethod
|
||||
def _get_path_file(cls, task, filp) -> str:
|
||||
rdentry = task.fs.get_root_dentry()
|
||||
rmnt = task.fs.get_root_mnt()
|
||||
dentry = filp.get_dentry()
|
||||
vfsmnt = filp.get_vfsmnt()
|
||||
|
||||
return LinuxUtilities._do_get_path(rdentry, rmnt, dentry, vfsmnt)
|
||||
path = "/" + "/".join(reversed(path_reversed))
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def _get_new_sock_pipe_path(cls, context, task, filp) -> str:
|
||||
"""Returns the sock pipe pathname relative to the task's root directory.
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
task (task_struct): A reference task
|
||||
filp (file *): A pointer to a sock pipe open file
|
||||
|
||||
Returns:
|
||||
str: Sock pipe pathname relative to the task's root directory.
|
||||
"""
|
||||
dentry = filp.get_dentry()
|
||||
|
||||
kernel_module = cls.get_module_from_volobj_type(context, dentry)
|
||||
|
||||
sym_addr = dentry.d_op.d_dname
|
||||
|
||||
symbol_table_arr = sym_addr.vol.type_name.split("!")
|
||||
symbol_table = None
|
||||
if len(symbol_table_arr) == 2:
|
||||
symbol_table = symbol_table_arr[0]
|
||||
|
||||
for module_name in context.modules.get_modules_by_symbol_tables(symbol_table):
|
||||
kernel_module = context.modules[module_name]
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"No module using the symbol table {symbol_table}")
|
||||
|
||||
symbs = list(kernel_module.get_symbols_by_absolute_location(sym_addr))
|
||||
|
||||
if len(symbs) == 1:
|
||||
@@ -163,12 +187,26 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
|
||||
return ret
|
||||
|
||||
# a 'file' structure doesn't have enough information to properly restore its full path
|
||||
# we need the root mount information from task_struct to determine this
|
||||
@classmethod
|
||||
def path_for_file(cls, context, task, filp) -> str:
|
||||
"""Returns a file (or sock pipe) pathname relative to the task's root directory.
|
||||
|
||||
A 'file' structure doesn't have enough information to properly restore its
|
||||
full path we need the root mount information from task_struct to determine this
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
task (task_struct): A reference task
|
||||
filp (file *): A pointer to an open file
|
||||
|
||||
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 ""
|
||||
|
||||
@@ -348,3 +386,35 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
|
||||
return vmlinux.object(
|
||||
object_type=type_name, offset=container_addr, absolute=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_module_from_volobj_type(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
volobj: interfaces.objects.ObjectInterface,
|
||||
) -> interfaces.context.ModuleInterface:
|
||||
"""Get the vmlinux from a vol obj
|
||||
|
||||
Args:
|
||||
context: The context to retrieve required elements (layers, symbol tables) from
|
||||
volobj (vol object): A vol object
|
||||
|
||||
Raises:
|
||||
ValueError: If it cannot obtain any module from the symbol table
|
||||
|
||||
Returns:
|
||||
A kernel object (vmlinux)
|
||||
"""
|
||||
symbol_table_arr = volobj.vol.type_name.split("!", 1)
|
||||
symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None
|
||||
|
||||
module_names = context.modules.get_modules_by_symbol_tables(symbol_table)
|
||||
module_names = list(module_names)
|
||||
|
||||
if not module_names:
|
||||
raise ValueError(f"No module using the symbol table '{symbol_table}'")
|
||||
|
||||
kernel_module_name = module_names[0]
|
||||
kernel = context.modules[kernel_module_name]
|
||||
|
||||
return kernel
|
||||
|
||||
@@ -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, CAP_FULL
|
||||
from volatility3.framework import exceptions, objects, interfaces, symbols
|
||||
from volatility3.framework.layers import linear
|
||||
from volatility3.framework.objects import utility
|
||||
@@ -34,10 +35,8 @@ class module(generic.GenericIntelProcess):
|
||||
def get_init_size(self):
|
||||
if self.has_member("init_layout"):
|
||||
return self.init_layout.size
|
||||
|
||||
elif self.has_member("init_size"):
|
||||
return self.init_size
|
||||
|
||||
raise AttributeError(
|
||||
"module -> get_init_size: Unable to determine .init section size of module"
|
||||
)
|
||||
@@ -45,10 +44,8 @@ class module(generic.GenericIntelProcess):
|
||||
def get_core_size(self):
|
||||
if self.has_member("core_layout"):
|
||||
return self.core_layout.size
|
||||
|
||||
elif self.has_member("core_size"):
|
||||
return self.core_size
|
||||
|
||||
raise AttributeError(
|
||||
"module -> get_core_size: Unable to determine core size of module"
|
||||
)
|
||||
@@ -58,7 +55,6 @@ class module(generic.GenericIntelProcess):
|
||||
return self.core_layout.base
|
||||
elif self.has_member("module_core"):
|
||||
return self.module_core
|
||||
|
||||
raise AttributeError("module -> get_module_core: Unable to get module core")
|
||||
|
||||
def get_module_init(self):
|
||||
@@ -66,7 +62,6 @@ class module(generic.GenericIntelProcess):
|
||||
return self.init_layout.base
|
||||
elif self.has_member("module_init"):
|
||||
return self.module_init
|
||||
|
||||
raise AttributeError("module -> get_module_core: Unable to get module init")
|
||||
|
||||
def get_name(self):
|
||||
@@ -88,7 +83,6 @@ class module(generic.GenericIntelProcess):
|
||||
idx = 0
|
||||
while arr[idx]:
|
||||
idx = idx + 1
|
||||
|
||||
return idx
|
||||
|
||||
def get_sections(self):
|
||||
@@ -97,7 +91,6 @@ class module(generic.GenericIntelProcess):
|
||||
num_sects = self.sect_attrs.nsections
|
||||
else:
|
||||
num_sects = self._get_sect_count(self.sect_attrs.grp)
|
||||
|
||||
arr = self._context.object(
|
||||
self.get_symbol_table().name + constants.BANG + "array",
|
||||
layer_name=self.vol.layer_name,
|
||||
@@ -116,7 +109,6 @@ class module(generic.GenericIntelProcess):
|
||||
prefix = "Elf64_"
|
||||
else:
|
||||
prefix = "Elf32_"
|
||||
|
||||
elf_table_name = intermed.IntermediateSymbolTable.create(
|
||||
self.context,
|
||||
self.config_path,
|
||||
@@ -155,7 +147,6 @@ class module(generic.GenericIntelProcess):
|
||||
return self.kallsyms.symtab
|
||||
elif self.has_member("symtab"):
|
||||
return self.symtab
|
||||
|
||||
raise AttributeError("module -> symtab: Unable to get symtab")
|
||||
|
||||
@property
|
||||
@@ -164,7 +155,6 @@ class module(generic.GenericIntelProcess):
|
||||
return int(self.kallsyms.num_symtab)
|
||||
elif self.has_member("num_symtab"):
|
||||
return int(self.num_symtab)
|
||||
|
||||
raise AttributeError(
|
||||
"module -> num_symtab: Unable to determine number of symbols"
|
||||
)
|
||||
@@ -177,7 +167,6 @@ class module(generic.GenericIntelProcess):
|
||||
# Older kernels
|
||||
elif self.has_member("strtab"):
|
||||
return self.strtab
|
||||
|
||||
raise AttributeError("module -> strtab: Unable to get strtab")
|
||||
|
||||
|
||||
@@ -195,19 +184,15 @@ class task_struct(generic.GenericIntelProcess):
|
||||
pgd = self.mm.pgd
|
||||
except exceptions.InvalidAddressException:
|
||||
return None
|
||||
|
||||
if not isinstance(parent_layer, linear.LinearlyMappedLayer):
|
||||
raise TypeError(
|
||||
"Parent layer is not a translation layer, unable to construct process layer"
|
||||
)
|
||||
|
||||
dtb, layer_name = parent_layer.translate(pgd)
|
||||
if not dtb:
|
||||
return None
|
||||
|
||||
if preferred_name is None:
|
||||
preferred_name = self.vol.layer_name + f"_Process{self.pid}"
|
||||
|
||||
# Add the constructed layer and return the name
|
||||
return self._add_process_layer(
|
||||
self._context, dtb, config_prefix, preferred_name
|
||||
@@ -229,7 +214,6 @@ class task_struct(generic.GenericIntelProcess):
|
||||
vollog.info(
|
||||
f"adding vma: {start:x} {self.mm.brk:x} | {end:x} {self.mm.start_brk:x}"
|
||||
)
|
||||
|
||||
yield (start, end - start)
|
||||
|
||||
@property
|
||||
@@ -282,7 +266,6 @@ class fs_struct(objects.StructType):
|
||||
return self.root
|
||||
elif self.root.has_member("dentry"):
|
||||
return self.root.dentry
|
||||
|
||||
raise AttributeError("Unable to find the root dentry")
|
||||
|
||||
def get_root_mnt(self):
|
||||
@@ -291,17 +274,135 @@ class fs_struct(objects.StructType):
|
||||
return self.rootmnt
|
||||
elif self.root.has_member("mnt"):
|
||||
return self.root.mnt
|
||||
|
||||
raise AttributeError("Unable to find the root mount")
|
||||
|
||||
|
||||
class maple_tree(objects.StructType):
|
||||
# include/linux/maple_tree.h
|
||||
# Mask for Maple Tree Flags
|
||||
MT_FLAGS_HEIGHT_MASK = 0x7C
|
||||
MT_FLAGS_HEIGHT_OFFSET = 0x02
|
||||
|
||||
# Shift and mask to extract information from maple tree node pointers
|
||||
MAPLE_NODE_TYPE_SHIFT = 0x03
|
||||
MAPLE_NODE_TYPE_MASK = 0x0F
|
||||
MAPLE_NODE_POINTER_MASK = 0xFF
|
||||
|
||||
# types of Maple Tree Nodes
|
||||
MAPLE_DENSE = 0
|
||||
MAPLE_LEAF_64 = 1
|
||||
MAPLE_RANGE_64 = 2
|
||||
MAPLE_ARANGE_64 = 3
|
||||
|
||||
def get_slot_iter(self):
|
||||
"""Parse the Maple Tree and return every non zero slot."""
|
||||
maple_tree_offset = self.vol.offset & ~(self.MAPLE_NODE_POINTER_MASK)
|
||||
expected_maple_tree_depth = (
|
||||
self.ma_flags & self.MT_FLAGS_HEIGHT_MASK
|
||||
) >> self.MT_FLAGS_HEIGHT_OFFSET
|
||||
yield from self._parse_maple_tree_node(
|
||||
self.ma_root, maple_tree_offset, expected_maple_tree_depth
|
||||
)
|
||||
|
||||
def _parse_maple_tree_node(
|
||||
self,
|
||||
maple_tree_entry,
|
||||
parent,
|
||||
expected_maple_tree_depth,
|
||||
seen=set(),
|
||||
current_depth=1,
|
||||
):
|
||||
"""Recursively parse Maple Tree Nodes and yield all non empty slots"""
|
||||
|
||||
# protect against unlikely loop
|
||||
if maple_tree_entry in seen:
|
||||
vollog.warning(
|
||||
f"The mte {hex(maple_tree_entry)} has all ready been seen, no further results will be produced for this node."
|
||||
)
|
||||
return
|
||||
else:
|
||||
seen.add(maple_tree_entry)
|
||||
# check if we have exceeded the expected depth of this maple tree.
|
||||
# e.g. when current_depth is larger than expected_maple_tree_depth there may be an issue.
|
||||
# it is normal that expected_maple_tree_depth is equal to current_depth.
|
||||
if expected_maple_tree_depth < current_depth:
|
||||
vollog.warning(
|
||||
f"The depth for the maple tree at {hex(self.vol.offset)} is {expected_maple_tree_depth}, however when parsing the nodes "
|
||||
f"a depth of {current_depth} was reached. This is unexpected and may lead to incorrect results."
|
||||
)
|
||||
# parse the mte to extract the pointer value, node type, and leaf status
|
||||
pointer = maple_tree_entry & ~(self.MAPLE_NODE_POINTER_MASK)
|
||||
node_type = (
|
||||
maple_tree_entry >> self.MAPLE_NODE_TYPE_SHIFT
|
||||
) & self.MAPLE_NODE_TYPE_MASK
|
||||
|
||||
# create a pointer object for the node parent mte (note this will include flags in the low bits)
|
||||
symbol_table_name = self.get_symbol_table_name()
|
||||
node_parent_mte = self._context.object(
|
||||
symbol_table_name + constants.BANG + "pointer",
|
||||
layer_name=self.vol.native_layer_name,
|
||||
offset=pointer,
|
||||
)
|
||||
|
||||
# extract the actual pointer to the parent of this node
|
||||
node_parent_pointer = node_parent_mte & ~(self.MAPLE_NODE_POINTER_MASK)
|
||||
|
||||
# verify that the node_parent_pointer correctly points to the parent
|
||||
assert node_parent_pointer == parent
|
||||
|
||||
# create a node object
|
||||
node = self._context.object(
|
||||
symbol_table_name + constants.BANG + "maple_node",
|
||||
layer_name=self.vol.layer_name,
|
||||
offset=pointer,
|
||||
)
|
||||
|
||||
# parse the slots based on the node type
|
||||
if node_type == self.MAPLE_DENSE:
|
||||
for slot in node.alloc.slot:
|
||||
if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0:
|
||||
yield slot
|
||||
elif node_type == self.MAPLE_LEAF_64:
|
||||
for slot in node.mr64.slot:
|
||||
if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0:
|
||||
yield slot
|
||||
elif node_type == self.MAPLE_RANGE_64:
|
||||
for slot in node.mr64.slot:
|
||||
if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0:
|
||||
yield from self._parse_maple_tree_node(
|
||||
slot,
|
||||
pointer,
|
||||
expected_maple_tree_depth,
|
||||
seen,
|
||||
current_depth + 1,
|
||||
)
|
||||
elif node_type == self.MAPLE_ARANGE_64:
|
||||
for slot in node.ma64.slot:
|
||||
if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0:
|
||||
yield from self._parse_maple_tree_node(
|
||||
slot,
|
||||
pointer,
|
||||
expected_maple_tree_depth,
|
||||
seen,
|
||||
current_depth + 1,
|
||||
)
|
||||
else:
|
||||
# unkown maple node type
|
||||
raise AttributeError(
|
||||
f"Unkown Maple Tree node type {node_type} at offset {hex(pointer)}."
|
||||
)
|
||||
|
||||
|
||||
class mm_struct(objects.StructType):
|
||||
def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Returns an iterator for the mmap list member of an mm_struct."""
|
||||
|
||||
if not self.has_member("mmap"):
|
||||
raise AttributeError(
|
||||
"get_mmap_iter called on mm_struct where no mmap member exists."
|
||||
)
|
||||
if not self.mmap:
|
||||
return
|
||||
|
||||
yield self.mmap
|
||||
|
||||
seen = {self.mmap.vol.offset}
|
||||
@@ -312,6 +413,33 @@ class mm_struct(objects.StructType):
|
||||
seen.add(link.vol.offset)
|
||||
link = link.vm_next
|
||||
|
||||
def get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Returns an iterator for the mm_mt member of an mm_struct."""
|
||||
|
||||
if not self.has_member("mm_mt"):
|
||||
raise AttributeError(
|
||||
"get_maple_tree_iter called on mm_struct where no mm_mt member exists."
|
||||
)
|
||||
symbol_table_name = self.get_symbol_table_name()
|
||||
for vma_pointer in self.mm_mt.get_slot_iter():
|
||||
# convert pointer to vm_area_struct and yield
|
||||
vma = self._context.object(
|
||||
symbol_table_name + constants.BANG + "vm_area_struct",
|
||||
layer_name=self.vol.native_layer_name,
|
||||
offset=vma_pointer,
|
||||
)
|
||||
yield vma
|
||||
|
||||
def get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Returns an iterator for the VMAs in an mm_struct. Automatically choosing the mmap or mm_mt as required."""
|
||||
|
||||
if self.has_member("mmap"):
|
||||
yield from self.get_mmap_iter()
|
||||
elif self.has_member("mm_mt"):
|
||||
yield from self.get_maple_tree_iter()
|
||||
else:
|
||||
raise AttributeError("Unable to find mmap or mm_mt in mm_struct")
|
||||
|
||||
|
||||
class super_block(objects.StructType):
|
||||
# include/linux/kdev_t.h
|
||||
@@ -418,7 +546,6 @@ class vm_area_struct(objects.StructType):
|
||||
retval = retval + char
|
||||
else:
|
||||
retval = retval + "-"
|
||||
|
||||
return retval
|
||||
|
||||
# only parse the rwx bits
|
||||
@@ -432,7 +559,6 @@ class vm_area_struct(objects.StructType):
|
||||
def get_page_offset(self) -> int:
|
||||
if self.vm_file == 0:
|
||||
return 0
|
||||
|
||||
return self.vm_pgoff << constants.linux.PAGE_SHIFT
|
||||
|
||||
def get_name(self, context, task):
|
||||
@@ -449,7 +575,6 @@ class vm_area_struct(objects.StructType):
|
||||
fname = "[vdso]"
|
||||
else:
|
||||
fname = "Anonymous Mapping"
|
||||
|
||||
return fname
|
||||
|
||||
# used by malfind
|
||||
@@ -460,10 +585,8 @@ class vm_area_struct(objects.StructType):
|
||||
|
||||
if flags_str == "rwx":
|
||||
ret = True
|
||||
|
||||
elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0:
|
||||
ret = True
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -473,12 +596,10 @@ class qstr(objects.StructType):
|
||||
str_length = self.len + 1 # Maximum length should include null terminator
|
||||
else:
|
||||
str_length = 255
|
||||
|
||||
try:
|
||||
ret = objects.utility.pointer_to_string(self.name, str_length)
|
||||
except (exceptions.InvalidAddressException, ValueError):
|
||||
ret = ""
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -509,7 +630,6 @@ class dentry(objects.StructType):
|
||||
"""
|
||||
if self.vol.offset == old_dentry:
|
||||
return True
|
||||
|
||||
return self.d_ancestor(old_dentry)
|
||||
|
||||
def d_ancestor(self, ancestor_dentry):
|
||||
@@ -527,10 +647,8 @@ class dentry(objects.StructType):
|
||||
):
|
||||
if current_dentry.d_parent == ancestor_dentry.vol.offset:
|
||||
return current_dentry
|
||||
|
||||
dentry_seen.add(current_dentry.vol.offset)
|
||||
current_dentry = current_dentry.d_parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -544,6 +662,7 @@ class struct_file(objects.StructType):
|
||||
raise AttributeError("Unable to find file -> dentry")
|
||||
|
||||
def get_vfsmnt(self) -> interfaces.objects.ObjectInterface:
|
||||
"""Returns the fs (vfsmount) where this file is mounted"""
|
||||
if self.has_member("f_vfsmnt"):
|
||||
return self.f_vfsmnt
|
||||
elif self.has_member("f_path"):
|
||||
@@ -587,12 +706,10 @@ class list_head(objects.StructType, collections.abc.Iterable):
|
||||
link = getattr(self, direction).dereference()
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
|
||||
if not sentinel:
|
||||
yield self._context.object(
|
||||
symbol_type, layer, offset=self.vol.offset - relative_offset
|
||||
)
|
||||
|
||||
seen = {self.vol.offset}
|
||||
while link.vol.offset not in seen:
|
||||
obj = self._context.object(
|
||||
@@ -675,11 +792,70 @@ class mount(objects.StructType):
|
||||
raise AttributeError("Unable to find mount -> mount flags")
|
||||
|
||||
def get_mnt_parent(self):
|
||||
"""Gets the fs where we are mounted on
|
||||
|
||||
Returns:
|
||||
A mount pointer
|
||||
"""
|
||||
return self.mnt_parent
|
||||
|
||||
def get_mnt_mountpoint(self):
|
||||
"""Gets the dentry of the mountpoint
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
|
||||
return self.mnt_mountpoint
|
||||
|
||||
def get_parent_mount(self):
|
||||
return self.mnt.get_parent_mount()
|
||||
|
||||
def has_parent(self) -> bool:
|
||||
"""Checks if this mount has a parent
|
||||
|
||||
Returns:
|
||||
bool: 'True' if this mount has a parent
|
||||
"""
|
||||
return self.mnt_parent != self.vol.offset
|
||||
|
||||
def get_vfsmnt_current(self):
|
||||
"""Returns the fs where we are mounted on
|
||||
|
||||
Returns:
|
||||
A 'vfsmount'
|
||||
"""
|
||||
return self.mnt
|
||||
|
||||
def get_vfsmnt_parent(self):
|
||||
"""Gets the parent fs (vfsmount) to where it's mounted on
|
||||
|
||||
Returns:
|
||||
A 'vfsmount'
|
||||
"""
|
||||
|
||||
return self.get_mnt_parent().get_vfsmnt_current()
|
||||
|
||||
def get_dentry_current(self):
|
||||
"""Returns the root of the mounted tree
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
vfsmnt = self.get_vfsmnt_current()
|
||||
dentry = vfsmnt.mnt_root
|
||||
|
||||
return dentry
|
||||
|
||||
def get_dentry_parent(self):
|
||||
"""Returns the parent root of the mounted tree
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
|
||||
return self.get_mnt_parent().get_dentry_current()
|
||||
|
||||
def get_flags_access(self) -> str:
|
||||
return "ro" if self.get_mnt_flags() & self.MNT_READONLY else "rw"
|
||||
|
||||
@@ -703,9 +879,6 @@ class mount(objects.StructType):
|
||||
def get_devname(self) -> str:
|
||||
return utility.pointer_to_string(self.mnt_devname, count=255)
|
||||
|
||||
def has_parent(self) -> bool:
|
||||
return self.vol.offset != self.mnt_parent
|
||||
|
||||
def get_dominating_id(self, root) -> int:
|
||||
"""Get ID of closest dominating peer group having a representative under the given root."""
|
||||
mnt_seen = set()
|
||||
@@ -718,7 +891,6 @@ class mount(objects.StructType):
|
||||
peer = current_mnt.get_peer_under_root(self.mnt_ns, root)
|
||||
if peer and peer.vol.offset != 0:
|
||||
return peer.mnt_group_id
|
||||
|
||||
mnt_seen.add(current_mnt.vol.offset)
|
||||
current_mnt = current_mnt.mnt_master
|
||||
return 0
|
||||
@@ -734,12 +906,10 @@ class mount(objects.StructType):
|
||||
current_mnt.mnt.mnt_root, root
|
||||
):
|
||||
return current_mnt
|
||||
|
||||
mnt_seen.add(current_mnt.vol.offset)
|
||||
current_mnt = current_mnt.next_peer()
|
||||
if current_mnt.vol.offset == self.vol.offset:
|
||||
break
|
||||
|
||||
return None
|
||||
|
||||
def is_path_reachable(self, current_dentry, root):
|
||||
@@ -756,7 +926,6 @@ class mount(objects.StructType):
|
||||
current_dentry = current_mnt.mnt_mountpoint
|
||||
mnt_seen.add(current_mnt.vol.offset)
|
||||
current_mnt = current_mnt.mnt_parent
|
||||
|
||||
return current_mnt.mnt.vol.offset == root.mnt and current_dentry.is_subdir(
|
||||
root.dentry
|
||||
)
|
||||
@@ -783,24 +952,121 @@ class vfsmount(objects.StructType):
|
||||
and self.get_mnt_parent() != 0
|
||||
)
|
||||
|
||||
def _get_real_mnt(self):
|
||||
table_name = self.vol.type_name.split(constants.BANG)[0]
|
||||
mount_struct = f"{table_name}{constants.BANG}mount"
|
||||
offset = self._context.symbol_space.get_type(
|
||||
mount_struct
|
||||
).relative_child_offset("mnt")
|
||||
def _is_kernel_prior_to_struct_mount(self) -> bool:
|
||||
"""Helper to distinguish between kernels prior to version 3.3.8 that
|
||||
lacked the 'mount' structure and later versions that have it.
|
||||
|
||||
return self._context.object(
|
||||
mount_struct, self.vol.layer_name, offset=self.vol.offset - offset
|
||||
The 'mnt_parent' member was moved from struct 'vfsmount' to struct
|
||||
'mount' when the latter was introduced.
|
||||
|
||||
Alternatively, vmlinux.has_type('mount') can be used here but it is faster.
|
||||
|
||||
Returns:
|
||||
bool: 'True' if the kernel
|
||||
"""
|
||||
|
||||
return self.has_member("mnt_parent")
|
||||
|
||||
def is_equal(self, vfsmount_ptr) -> bool:
|
||||
"""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
|
||||
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 \*'.
|
||||
Typically, it's called from do_get_path().
|
||||
|
||||
Args:
|
||||
vfsmount_ptr (vfsmount \*): A pointer to a 'vfsmount'
|
||||
|
||||
Raises:
|
||||
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \*'
|
||||
|
||||
Returns:
|
||||
bool: 'True' if the given argument points to the the same 'vfsmount'
|
||||
as 'self'.
|
||||
"""
|
||||
if type(vfsmount_ptr) == objects.Pointer:
|
||||
return self.vol.offset == vfsmount_ptr
|
||||
else:
|
||||
raise exceptions.VolatilityException(
|
||||
"Unexpected argument type. It has to be a 'vfsmount *'"
|
||||
)
|
||||
|
||||
def _get_real_mnt(self):
|
||||
"""Gets the struct 'mount' containing this 'vfsmount'.
|
||||
|
||||
It should be only called from kernels >= 3.3.8 when 'struct mount' was introduced.
|
||||
|
||||
Returns:
|
||||
mount: the struct 'mount' containing this 'vfsmount'.
|
||||
"""
|
||||
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
|
||||
return linux.LinuxUtilities.container_of(
|
||||
self.vol.offset, "mount", "mnt", vmlinux
|
||||
)
|
||||
|
||||
def get_vfsmnt_current(self):
|
||||
"""Returns the current fs where we are mounted on
|
||||
|
||||
Returns:
|
||||
A vfsmount pointer
|
||||
"""
|
||||
return self.get_mnt_parent()
|
||||
|
||||
def get_vfsmnt_parent(self):
|
||||
"""Gets the parent fs (vfsmount) to where it's mounted on
|
||||
|
||||
Returns:
|
||||
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()
|
||||
else:
|
||||
return self._get_real_mnt().get_vfsmnt_parent()
|
||||
|
||||
def get_dentry_current(self):
|
||||
"""Returns the root of the mounted tree
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
if self._is_kernel_prior_to_struct_mount():
|
||||
return self.get_mnt_mountpoint()
|
||||
else:
|
||||
return self._get_real_mnt().get_dentry_current()
|
||||
|
||||
def get_dentry_parent(self):
|
||||
"""Returns the parent root of the mounted tree
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
if self._is_kernel_prior_to_struct_mount():
|
||||
return self.get_mnt_mountpoint()
|
||||
else:
|
||||
return self._get_real_mnt().get_mnt_mountpoint()
|
||||
|
||||
def get_mnt_parent(self):
|
||||
if self.has_member("mnt_parent"):
|
||||
"""Gets the mnt_parent member.
|
||||
|
||||
Returns:
|
||||
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
|
||||
else:
|
||||
return self._get_real_mnt().mnt_parent
|
||||
return self._get_real_mnt().get_mnt_parent()
|
||||
|
||||
def get_mnt_mountpoint(self):
|
||||
"""Gets the dentry of the mountpoint
|
||||
|
||||
Returns:
|
||||
A dentry pointer
|
||||
"""
|
||||
if self.has_member("mnt_mountpoint"):
|
||||
return self.mnt_mountpoint
|
||||
else:
|
||||
@@ -809,6 +1075,41 @@ class vfsmount(objects.StructType):
|
||||
def get_mnt_root(self):
|
||||
return self.mnt_root
|
||||
|
||||
def has_parent(self) -> bool:
|
||||
if self._is_kernel_prior_to_struct_mount():
|
||||
return self.mnt_parent != self.vol.offset
|
||||
else:
|
||||
return self._get_real_mnt().has_parent()
|
||||
|
||||
def get_mnt_sb(self):
|
||||
return self.mnt_sb
|
||||
|
||||
def get_flags_access(self) -> str:
|
||||
return "ro" if self.mnt_flags & mount.MNT_READONLY else "rw"
|
||||
|
||||
def get_flags_opts(self) -> Iterable[str]:
|
||||
flags = [
|
||||
mntflagtxt
|
||||
for mntflag, mntflagtxt in mount.MNT_FLAGS.items()
|
||||
if mntflag & self.mnt_flags != 0
|
||||
]
|
||||
return flags
|
||||
|
||||
def get_mnt_flags(self):
|
||||
return self.mnt_flags
|
||||
|
||||
def is_shared(self) -> bool:
|
||||
return self.get_mnt_flags() & mount.MNT_SHARED
|
||||
|
||||
def is_unbindable(self) -> bool:
|
||||
return self.get_mnt_flags() & mount.MNT_UNBINDABLE
|
||||
|
||||
def is_slave(self) -> bool:
|
||||
return self.mnt_master and self.mnt_master.vol.offset != 0
|
||||
|
||||
def get_devname(self) -> str:
|
||||
return utility.pointer_to_string(self.mnt_devname, count=255)
|
||||
|
||||
|
||||
class kobject(objects.StructType):
|
||||
def reference_count(self):
|
||||
@@ -817,7 +1118,6 @@ class kobject(objects.StructType):
|
||||
ret = refcnt.counter
|
||||
else:
|
||||
ret = refcnt.refs.counter
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -825,7 +1125,7 @@ class mnt_namespace(objects.StructType):
|
||||
def get_inode(self):
|
||||
if self.has_member("proc_inum"):
|
||||
return self.proc_inum
|
||||
elif self.ns.has_member("inum"):
|
||||
elif self.has_member("ns") and self.ns.has_member("inum"):
|
||||
return self.ns.inum
|
||||
else:
|
||||
raise AttributeError("Unable to find mnt_namespace inode")
|
||||
@@ -836,7 +1136,6 @@ class mnt_namespace(objects.StructType):
|
||||
if not self._context.symbol_space.has_type(mnt_type):
|
||||
# Old kernels ~ 2.6
|
||||
mnt_type = table_name + constants.BANG + "vfsmount"
|
||||
|
||||
for mount in self.list.to_list(mnt_type, "mnt_list"):
|
||||
yield mount
|
||||
|
||||
@@ -844,10 +1143,13 @@ class mnt_namespace(objects.StructType):
|
||||
class net(objects.StructType):
|
||||
def get_inode(self):
|
||||
if self.has_member("proc_inum"):
|
||||
# 3.8.13 <= kernel < 3.19.8
|
||||
return self.proc_inum
|
||||
elif self.ns.has_member("inum"):
|
||||
elif self.has_member("ns") and self.ns.has_member("inum"):
|
||||
# kernel >= 3.19.8
|
||||
return self.ns.inum
|
||||
else:
|
||||
# kernel < 3.8.13
|
||||
raise AttributeError("Unable to find net_namespace inode")
|
||||
|
||||
|
||||
@@ -861,7 +1163,6 @@ class socket(objects.StructType):
|
||||
)
|
||||
if not module_names:
|
||||
raise ValueError(f"No module using the symbol table {symbol_table}")
|
||||
|
||||
kernel_module_name = module_names[0]
|
||||
kernel = self._context.modules[kernel_module_name]
|
||||
return kernel
|
||||
@@ -871,7 +1172,6 @@ class socket(objects.StructType):
|
||||
kernel = self._get_vol_kernel()
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
socket_alloc = linux.LinuxUtilities.container_of(
|
||||
self.vol.offset, "socket_alloc", "socket", kernel
|
||||
)
|
||||
@@ -897,7 +1197,6 @@ class sock(objects.StructType):
|
||||
def get_inode(self):
|
||||
if not self.sk_socket:
|
||||
return 0
|
||||
|
||||
return self.sk_socket.get_inode()
|
||||
|
||||
def get_protocol(self):
|
||||
@@ -907,7 +1206,6 @@ class sock(objects.StructType):
|
||||
# Return the generic socket state
|
||||
if self.has_member("sk"):
|
||||
return self.sk.sk_socket.get_state()
|
||||
|
||||
return self.sk_socket.get_state()
|
||||
|
||||
|
||||
@@ -915,7 +1213,6 @@ class unix_sock(objects.StructType):
|
||||
def get_name(self):
|
||||
if not self.addr:
|
||||
return
|
||||
|
||||
sockaddr_un = self.addr.name.cast("sockaddr_un")
|
||||
saddr = str(utility.array_to_string(sockaddr_un.sun_path))
|
||||
return saddr
|
||||
@@ -951,7 +1248,6 @@ class inet_sock(objects.StructType):
|
||||
protocol = IP_PROTOCOLS.get(self.sk.sk_protocol)
|
||||
if self.get_family() == "AF_INET6":
|
||||
protocol = IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol)
|
||||
|
||||
return protocol
|
||||
|
||||
def get_state(self):
|
||||
@@ -982,7 +1278,6 @@ class inet_sock(objects.StructType):
|
||||
dport_le = sk_common.skc_dport
|
||||
else:
|
||||
return
|
||||
|
||||
return socket_module.htons(dport_le)
|
||||
|
||||
def get_src_addr(self):
|
||||
@@ -1001,7 +1296,6 @@ class inet_sock(objects.StructType):
|
||||
saddr = self.pinet6.saddr
|
||||
else:
|
||||
return
|
||||
|
||||
parent_layer = self._context.layers[self.vol.layer_name]
|
||||
try:
|
||||
addr_bytes = parent_layer.read(saddr.vol.offset, addr_size)
|
||||
@@ -1010,7 +1304,6 @@ class inet_sock(objects.StructType):
|
||||
f"Unable to read socket src address from {saddr.vol.offset:#x}"
|
||||
)
|
||||
return
|
||||
|
||||
return socket_module.inet_ntop(family, addr_bytes)
|
||||
|
||||
def get_dst_addr(self):
|
||||
@@ -1032,7 +1325,6 @@ class inet_sock(objects.StructType):
|
||||
addr_size = 16
|
||||
else:
|
||||
return
|
||||
|
||||
parent_layer = self._context.layers[self.vol.layer_name]
|
||||
try:
|
||||
addr_bytes = parent_layer.read(daddr.vol.offset, addr_size)
|
||||
@@ -1041,7 +1333,6 @@ class inet_sock(objects.StructType):
|
||||
f"Unable to read socket dst address from {daddr.vol.offset:#x}"
|
||||
)
|
||||
return
|
||||
|
||||
return socket_module.inet_ntop(family, addr_bytes)
|
||||
|
||||
|
||||
@@ -1055,6 +1346,26 @@ class netlink_sock(objects.StructType):
|
||||
# Return the generic socket state
|
||||
return self.sk.sk_socket.get_state()
|
||||
|
||||
def get_portid(self):
|
||||
if self.has_member("pid"):
|
||||
# kernel < 3.7.10
|
||||
return self.pid
|
||||
if self.has_member("portid"):
|
||||
# kernel >= 3.7.10
|
||||
return self.portid
|
||||
else:
|
||||
raise AttributeError("Unable to find a source port id")
|
||||
|
||||
def get_dst_portid(self):
|
||||
if self.has_member("dst_pid"):
|
||||
# kernel < 3.7.10
|
||||
return self.dst_pid
|
||||
if self.has_member("dst_portid"):
|
||||
# kernel >= 3.7.10
|
||||
return self.dst_portid
|
||||
else:
|
||||
raise AttributeError("Unable to find a destination port id")
|
||||
|
||||
|
||||
class vsock_sock(objects.StructType):
|
||||
def get_protocol(self):
|
||||
@@ -1101,3 +1412,127 @@ class xdp_sock(objects.StructType):
|
||||
def get_state(self):
|
||||
# xdp_sock.state is an enum
|
||||
return self.state.lookup()
|
||||
|
||||
|
||||
class bpf_prog(objects.StructType):
|
||||
def get_type(self):
|
||||
# The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to
|
||||
# 4.1.52 before it was moved to `bpf_prog::type`
|
||||
if self.has_member("type"):
|
||||
# kernel >= 4.1.52
|
||||
return self.type
|
||||
|
||||
if self.has_member("aux") and self.aux:
|
||||
if self.aux.has_member("prog_type"):
|
||||
# 3.18.140 <= kernel < 4.1.52
|
||||
return self.aux.prog_type
|
||||
|
||||
# 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
|
||||
|
||||
@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.
|
||||
"""
|
||||
# In kernels 2.6.25.20 the kernel_cap_struct::cap became and array
|
||||
cap_value = self.cap[0] if isinstance(self.cap, objects.Array) else self.cap
|
||||
return cap_value & 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