Merge pull request #1977 from volatilityfoundation/release/v2.28.0

Release/v2.28.0
This commit is contained in:
ikelos
2026-04-30 20:29:02 +01:00
committed by GitHub
47 changed files with 2022 additions and 301 deletions
-15
View File
@@ -1,15 +0,0 @@
name: Black python formatter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check --diff --verbose"
src: "./volatility3"
# FIXME: Remove when Volatility3 minimum Python version is >3.8
version: "24.8.0"
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
fail-fast: false
matrix:
host: [ ubuntu-latest, windows-latest ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.10", "3.11", "3.12", "3.13" ]
steps:
- uses: actions/checkout@v4
+3 -1
View File
@@ -4,7 +4,7 @@ name: Ruff
on: [push, pull_request]
jobs:
lint:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -13,3 +13,5 @@ jobs:
with:
args: check
src: "."
- run: "ruff format --check --diff"
+5 -5
View File
@@ -26,19 +26,19 @@ jobs:
run: |
mkdir test_images
cd test_images
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz"
curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux-sample-1.bin.gz"
gunzip linux-sample-1.bin.gz
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz"
curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-xp-laptop-2005-06-25.img.gz"
gunzip win-xp-laptop-2005-06-25.img.gz
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-10_19041-2025_03.dmp.gz"
curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-10_19041-2025_03.dmp.gz"
gunzip win-10_19041-2025_03.dmp.gz
cd -
- name: Download and Extract symbols
run: |
cd ./volatility3/symbols
curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip
curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/symbols_win-10_19041-2025_03.zip
curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip
curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/symbols_win-10_19041-2025_03.zip
unzip linux.zip
unzip symbols_win-10_19041-2025_03.zip
cd -
+1 -1
View File
@@ -5,7 +5,7 @@ The coding standards for volatility are mostly by our linter and our code format
All code submissions will be vetted automatically through tests from both and the submission will not be accepted if either of these fail.
Code Linter: Ruff
Code Formatter: Black
Code Formatter: Ruff
In addition, there are some coding practices that we employ to prevent specific failure cases and ensure consistency across the codebase. These are documented below along with the rationale for the decision.
+6 -6
View File
@@ -65,19 +65,19 @@ pip install -e ".[dev]"
Symbol table packs for the various operating systems are available for download at:
<https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip>
[windows.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/windows.zip)
<https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>
[mac.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/mac.zip)
<https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip>
[linux.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip)
The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at:
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA256SUMS>
[SHA256SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA256SUMS)
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA1SUMS>
[SHA1SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA1SUMS)
<https://downloads.volatilityfoundation.org/volatility3/symbols/MD5SUMS>
[MD5SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/MD5SUMS)
Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file).
+34
View File
@@ -618,3 +618,37 @@ class TestLinuxPscallstack:
rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel",
out,
)
class TestLinuxSockscan:
def test_linux_sockscan(self, volatility, python):
# designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6
image = LinuxSamples.LINUX_GENERIC.value.path
rc, out, err = test_volatility.runvol_plugin(
"linux.sockscan.Sockscan", image, volatility, python
)
# ensure that multiple unix paths for sockets have been found
assert (
len(
re.findall(
rb"(/[ -~]+?){1,8}",
out,
)
)
>= 10
)
# ensure that multiple IPv4 addresses have been found
assert (
len(
re.findall(
rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}",
out,
)
)
>= 10
)
assert out.count(b"\n") >= 50
assert rc == 0
+5
View File
@@ -5,7 +5,12 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import sys
import volatility3.cli
if __name__ == "__main__":
# Ensure stdout/stderr use UTF-8 to avoid output encoding errors on Windows systems
sys.stderr.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
volatility3.cli.main()
+6 -5
View File
@@ -10,8 +10,8 @@ import string
import sys
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union
from volatility3.cli import text_filter
from volatility3.cli import text_filter
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.renderers import format_hints
@@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer):
accumulator.append((node.path_depth, line))
return accumulator
final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = (
[]
)
final_output: List[
Tuple[int, Dict[interfaces.renderers.Column, list[str]]]
] = []
if not grid.populated:
grid.populate(visitor, final_output)
else:
@@ -598,7 +598,8 @@ class JsonRenderer(CLIRenderer):
if self.filter and self.filter.filter(line):
return accumulator
if node.parent:
# Only add if the parent hasn't been filtered out
if node.parent and node.parent.path in acc_map:
acc_map[node.parent.path]["__children"].append(node_dict)
else:
final_tree.append(node_dict)
+1 -3
View File
@@ -165,9 +165,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
"init_mm"
).address and init_task.tasks.next.cast(
"long unsigned int"
) == init_task.tasks.prev.cast(
"long unsigned int"
):
) == init_task.tasks.prev.cast("long unsigned int"):
# The idle task steals `mm` from previously running task, i.e.,
# `init_mm` is only used as long as no CPU has ever been idle.
# This catches cases where we found a fragment of the
+32 -8
View File
@@ -153,8 +153,7 @@ class DtbSelfRefPae(DtbSelfReferential):
# Mask off the page bits of top level page map
page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4
page_table = data[
top_pae_page
- data_offset : top_pae_page
top_pae_page - data_offset : top_pae_page
- data_offset
+ (4 * self.ptr_size)
]
@@ -200,7 +199,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
(
"Detecting Self-referential pointer for recent windows",
[DtbSelfRef64bit()],
[(0x150000, 0x150000), (0x650000, 0xA0000)],
[(0x150000, 0x150000), (0x550000, 0x1A0000), (0x900000, 0x100000)],
),
(
"Older windows fixed location self-referential pointers",
@@ -287,28 +286,53 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
"""Key used to sort by tests"""
return tests.index(x[0]), x[1]
def get_max_pointer(page_table, test, ptr_size: int):
"""Determines a pointer from a page_table"""
max_ptr = 0
def get_valid_page_table_pointers(page_table, ptr_size: int):
"""Yields valid pointers from a page table"""
for index in range(0, len(page_table), ptr_size):
pointer = struct.unpack(
test.ptr_struct, page_table[index : index + ptr_size]
)[0]
# Make sure the pointer is valid, ignore large pages which would require more calculation
if pointer & 0x1 and not pointer & 0x80:
yield pointer
def get_max_pointer(page_table, test, ptr_size: int):
"""Determines a pointer from a page_table"""
max_ptr = 0
for pointer in get_valid_page_table_pointers(page_table, ptr_size):
max_ptr = max(
max_ptr,
(pointer ^ (pointer & 0xFFF))
% test.layer_type.maximum_address,
(pointer ^ (pointer & 0xFFF)) % test.layer_type.maximum_address,
)
return max_ptr
def page_table_is_dummy(page_table, ptr_size: int):
"""Verify that a page table has at least 12 valid pointers"""
valid_pointers = 0
for _ in get_valid_page_table_pointers(page_table, ptr_size):
valid_pointers += 1
# 10 is an arbitrary constant
if valid_pointers >= 10:
# Do not consume the entire generator to enhance performance
return False
vollog.debug(f"Found {valid_pointers} valid pointers")
return True
hits = sorted(list(hits), key=sort_by_tests)
vollog.debug(f"WindowsIntelStacker hits: {hits}")
for test, page_map_offset in hits:
# Turn the page tables into integers and find the largest one
page_table = base_layer.read(page_map_offset, 0x1000)
ptr_size = struct.calcsize(test.ptr_struct)
# Modern windows can have a dummy page table with only about 2 entries, so sanity check
if page_table_is_dummy(page_table, ptr_size):
vollog.debug(
f"DTB {page_map_offset:x} contains less than 12 valid pointers, ignoring"
)
continue
max_pointer = get_max_pointer(page_table, test, ptr_size)
if max_pointer <= base_layer.maximum_address:
+15 -12
View File
@@ -25,6 +25,18 @@ from volatility3.framework.constants._version import (
REQUIRED_PYTHON_VERSION = (3, 8, 0)
CACHE_PATH = os.path.join(
os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"),
"volatility3",
)
"""Default path to store cached data"""
if sys.platform == "win32":
CACHE_PATH = os.path.realpath(
os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3")
)
os.makedirs(CACHE_PATH, exist_ok=True)
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")),
@@ -34,6 +46,9 @@ PLUGINS_PATH = [
SYMBOL_BASEPATHS = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "symbols")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "symbols")),
os.path.abspath(
os.path.join(CACHE_PATH, "symbols")
), # User cache fallback for automatically downloaded temporary symbols
]
"""Default list of paths to load symbols from (volatility3/symbols and volatility3/framework/symbols)"""
@@ -71,21 +86,9 @@ LOGLEVEL_VVVV = 6
"""Logging level for four levels of detail: -vvvvvv"""
CACHE_PATH = os.path.join(
os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"),
"volatility3",
)
"""Default path to store cached data"""
SQLITE_CACHE_PERIOD = "-3 days"
"""SQLite time modifier for how long each item is valid in the cache for"""
if sys.platform == "win32":
CACHE_PATH = os.path.realpath(
os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3")
)
os.makedirs(CACHE_PATH, exist_ok=True)
IDENTIFIERS_FILENAME = "identifier.cache"
"""Default location to record information about available identifiers"""
+1 -1
View File
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 27 # Number of changes that only add to the interface
VERSION_MINOR = 28 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -9,6 +9,10 @@ Linux-specific values that aren't found in debug symbols
import enum
from dataclasses import dataclass
# Exec argument limits
# Ref: include/uapi/linux/binfmts.h (linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940)
MAX_ARG_STRLEN = 32 * 4096
KERNEL_NAME = "__kernel__"
"""The value hard coded from the Linux Kernel (hence not extracted from the layer itself)"""
@@ -432,6 +436,17 @@ VMCOREINFO_MAGIC = b"VMCOREINFO\x00"
VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00"
OSRELEASE_TAG = b"OSRELEASE="
ATTRIBUTE_NAME_MAX_SIZE = 255
"""
In 5.9-rc1+, the Linux kernel limits the READ size of a section bin_attribute name to MODULE_SECT_READ_SIZE:
- https://elixir.bootlin.com/linux/v6.15-rc4/source/kernel/module/sysfs.c#L106
- https://github.com/torvalds/linux/commit/11990a5bd7e558e9203c1070fc52fb6f0488e75b
However, the raw section name loaded from the .ko ELF can in theory be thousands of characters,
and unless we do a NULL terminated search we can't set a perfect value.
"""
@dataclass
class TaintFlag:
+1 -2
View File
@@ -160,8 +160,7 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer):
if frame_type == 0xFF:
if (
data[
offset
+ frame_header_len : offset
offset + frame_header_len : offset
+ frame_header_len
+ frame_size
]
+57 -21
View File
@@ -10,7 +10,7 @@ import struct
from typing import Any, Dict, Iterable, List, Optional, Tuple
from volatility3 import classproperty
from volatility3.framework import exceptions, interfaces, constants
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import linear
@@ -38,7 +38,7 @@ class Intel(linear.LinearlyMappedLayer):
# NOTE: _maxphyaddr is MAXPHYADDR as defined in the Intel specs *NOT* the maximum physical address
_maxphyaddr = 32
_maxvirtaddr = _maxphyaddr
_structure = [("page directory", 10, False), ("page table", 10, True)]
_structure = [("page directory", 10, True), ("page table", 10, False)]
_direct_metadata = collections.ChainMap(
{"architecture": "Intel32"},
{"mapped": True},
@@ -221,18 +221,6 @@ class Intel(linear.LinearlyMappedLayer):
entry,
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Check if we're a large page
if large_page and (entry & self._PAGE_PSE):
# Mask off the PAT bit
if entry & self._PAGE_PAT_LARGE:
entry -= self._PAGE_PAT_LARGE
# We're a large page, the rest is finished below
# If we want to implement PSE-36, it would need to be done here
break
# Figure out how much of the offset we should be using
start = position
position -= size
index = self._mask(page_address, start, position + 1) >> (position + 1)
# Grab the base address of the table we'll be getting the next entry from
base_address = self._mask(
@@ -249,6 +237,11 @@ class Intel(linear.LinearlyMappedLayer):
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Figure out how much of the offset we should be using
start = position
position -= size
index = self._mask(page_address, start, position + 1) >> (position + 1)
# Read the data for the next entry
entry_data_start = index << self._index_shift
entry_data = table[entry_data_start : entry_data_start + self._entry_size]
@@ -262,16 +255,52 @@ class Intel(linear.LinearlyMappedLayer):
# Read out the new entry from memory
(entry,) = struct.unpack(self._entry_format, entry_data)
# Check if we're a large page
if large_page and (entry & self._PAGE_PSE):
# Mask off the PAT bit
if entry & self._PAGE_PAT_LARGE:
entry -= self._PAGE_PAT_LARGE
# We're a large page, the rest is finished below
# If we want to implement PSE-36, it would need to be done here
break
return entry, position
@functools.lru_cache(maxsize=1025)
def _get_valid_table(self, base_address: int) -> Optional[bytes]:
"""Extracts the table, validates it and returns it if it's valid."""
try:
table = self._context.layers.read(
self._base_layer, base_address, self.page_size
)
except exceptions.InvalidAddressException:
return None
####
# If the table is entirely duplicates, then mark the whole table as bad
# This is because Windows 10 onwards has a tendency to map unused pages as present
# This had the following consequences:
# - Used very litle physical memory
# - Exploded virtual memory
# - Causes *scan plugins to take multiple hours to complete even on small images
# Previous versions of volatility would ignore a page during a scan when it matched
# the one directly preceding it in physical memory.
# This could trip if only two pages were identical and still required enumerating all
# the invalid pages (which itself was quite time consuming)
# For this reason, volatility 3 shifted to looking at entire page tables (1,024 pages)
# and if all the pages mapped to the same place the table wouuld be skipped
# This could also be applied to the Directory level as well as the Table level, allowing
# Volatility to skip huge sections of virtual memory very efficiently, without missing
# any pages that were distinct within a particular page table (or directory).
# In order to work at this level, the logic was moved out of the scanning component and
# directly into the layer logic itself. This does have the side effect of preventing
# entirely duplicated page tables from reporting as present, however, the trade off between
# Windows 10+ reduced scanning times (common amongst scan plugins) versus incorrectly reporting
# entire page tables of identically mapped repeating *valid* data (rare) was accepted in favour
# of the more common occurance.
if table == table[: self._entry_size] * self._entry_number:
return None
return table
@@ -371,12 +400,18 @@ class Intel(linear.LinearlyMappedLayer):
yield offset, length, mapped_offset, length, layer_name
return None
while length > 0:
skip_mask = None
try:
chunk_offset, page_size, layer_name = self._translate(offset)
chunk_size = min(page_size - (chunk_offset % page_size), length)
# Page align the chunk size value
chunk_size = min(page_size - (offset % page_size), length)
if not self._context.layers[layer_name].is_valid(
chunk_offset, chunk_size
):
# Virtual -> physical is contiguous in the chunk_size range.
# If we fail, we can jump directly to the end as we know all bytes in between
# aren't mapped (virtually and) physically anyway.
skip_mask = chunk_size - 1
raise exceptions.InvalidAddressException(
layer_name=layer_name, invalid_address=chunk_offset
)
@@ -386,12 +421,13 @@ class Intel(linear.LinearlyMappedLayer):
) as excp:
if not ignore_errors:
raise
# We can jump more if we know where the page fault failed
if skip_mask is None:
# We can jump more if we know where the page fault occured
if isinstance(excp, exceptions.PagedInvalidAddressException):
mask = (1 << excp.invalid_bits) - 1
skip_mask = (1 << excp.invalid_bits) - 1
else:
mask = (1 << self._page_size_in_bits) - 1
length_diff = mask + 1 - (offset & mask)
skip_mask = (1 << self._page_size_in_bits) - 1
length_diff = skip_mask + 1 - (offset & skip_mask)
length -= length_diff
offset += length_diff
else:
@@ -429,7 +465,7 @@ class IntelPAE(Intel):
_structure = [
("page directory pointer", 2, False),
("page directory", 9, True),
("page table", 9, True),
("page table", 9, False),
]
_direct_metadata = collections.ChainMap({"pae": True}, Intel._direct_metadata)
@@ -449,7 +485,7 @@ class Intel32e(Intel):
("page map layer 4", 9, False),
("page directory pointer", 9, True),
("page directory", 9, True),
("page table", 9, True),
("page table", 9, False),
]
+3 -3
View File
@@ -948,9 +948,9 @@ class AggregateType(interfaces.objects.ObjectInterface):
if isinstance(cls, agg_type):
agg_name = agg_type.__name__
assert isinstance(
members, collections.abc.Mapping
), f"{agg_name} members parameter must be a mapping: {type(members)}"
assert isinstance(members, collections.abc.Mapping), (
f"{agg_name} members parameter must be a mapping: {type(members)}"
)
assert all(
(isinstance(member, tuple) and len(member) == 2)
for member in members.values()
+63 -1
View File
@@ -3,11 +3,13 @@
#
import re
import logging
from typing import Optional, Union
from volatility3.framework import interfaces, objects, constants, exceptions
vollog = logging.getLogger(__name__)
def rol(value: int, count: int, max_bits: int = 64) -> int:
"""A rotate-left instruction in Python"""
@@ -250,3 +252,63 @@ def array_of_pointers(
).clone()
subtype_pointer.update_vol(subtype=subtype)
return array.cast("array", count=count, subtype=subtype_pointer)
def dynamically_sized_array_of_pointers(
context: interfaces.context.ContextInterface,
array: interfaces.objects.ObjectInterface,
subtype: Union[str, interfaces.objects.Template],
iterator_guard_value: int,
stop_value: int = 0,
stop_on_invalid_pointers: bool = True,
) -> interfaces.objects.ObjectInterface:
"""Iterates over a dynamically sized array of pointers (e.g. NULL-terminated).
Array iteration should always be performed with an arbitrary guard value as maximum size,
to prevent running forever in case something unexpected happens.
Args:
context: The context on which to operate.
array: The object to cast to an array.
iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing.
subtype: The subtype of the array's pointers.
stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays).
stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays
that are known to have smeared entries before the end.
Returns:
An array of pointer objects
"""
new_count = 0
sym_table_name = array.get_symbol_table_name()
sym_table = context.symbol_space[sym_table_name]
ptr_size = sym_table.get_type("pointer").size
layer_name = array.vol.layer_name
offset = array.vol.offset
entry = None
while entry != stop_value and new_count < iterator_guard_value:
try:
entry = context.object(
sym_table_name + constants.BANG + "pointer",
offset=offset,
layer_name=layer_name,
)
except exceptions.InvalidAddressException:
break
if not entry.is_readable() and stop_on_invalid_pointers:
break
offset += ptr_size
new_count += 1
else:
vollog.log(
constants.LOGLEVEL_V,
f"""Iterator guard value {iterator_guard_value} reached while iterating over array at offset {array.vol.offset:#x}.\
This means that there is a bug (e.g. smearing) with this array, or that it may contain valid entries past the iterator guard value.""",
)
# Leverage the "Array" object instead of returning a Python list
return array_of_pointers(
array=array, count=new_count, subtype=subtype, context=context
)
+28 -1
View File
@@ -4,10 +4,11 @@
import logging
from typing import List
from volatility3.framework import interfaces, renderers, layers
from volatility3.framework import constants, interfaces, layers, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanners
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols.windows import pdbutil
vollog = logging.getLogger(__name__)
@@ -16,6 +17,7 @@ class Banners(interfaces.plugins.PluginInterface):
"""Attempts to identify potential linux banners in an image"""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -28,6 +30,11 @@ class Banners(interfaces.plugins.PluginInterface):
component=scanners.RegExScanner,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="pdb_signature_scanner",
component=pdbutil.PdbSignatureScanner,
version=(1, 0, 0),
),
]
def _generator(self):
@@ -42,6 +49,7 @@ class Banners(interfaces.plugins.PluginInterface):
cls, context: interfaces.context.ContextInterface, layer_name: str
):
"""Identifies banners from a memory image"""
# Look for likely linux/mac banners
layer = context.layers[layer_name]
for offset in layer.scan(
context=context,
@@ -64,6 +72,25 @@ class Banners(interfaces.plugins.PluginInterface):
format_hints.Hex(offset),
str(data, encoding="latin-1", errors="?"),
)
yield from cls.locate_windows_banners(context, layer_name)
@classmethod
def locate_windows_banners(
cls, context: interfaces.context.ContextInterface, layer_name: str
):
layer = context.layers[layer_name]
kernel_pdb_names = [
bytes(name + ".pdb", "utf-8")
for name in constants.windows.KERNEL_MODULE_NAMES
]
for guid, age, pdb_name, offset in layer.scan(
context=context,
scanner=pdbutil.PdbSignatureScanner(kernel_pdb_names),
):
yield (
format_hints.Hex(offset),
f"{pdb_name.decode('latin-1')}|{guid}|{age}",
)
def run(self):
return renderers.TreeGrid(
@@ -88,7 +88,12 @@ class Malfind(interfaces.plugins.PluginInterface):
for page_addr in malicious_pages:
offset = page_addr - vma.vm_start
data = proc_layer.read(page_addr, dump_size, pad=True)
yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset
yield (
vma,
f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}",
data,
offset,
)
else:
# Original behaviour - Dump the start of the region (not necessarily matching the dirty page)
data = proc_layer.read(vma.vm_start, dump_size, pad=True)
@@ -0,0 +1,300 @@
# This file is Copyright 2025 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 pathlib import PurePosixPath
from typing import Optional, Tuple, Iterator
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.constants import linux as linux_constants
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
from volatility3.framework.symbols import linux
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class ProcessSpoofing(plugins.PluginInterface):
"""Detects process spoofing by comparing executable path to cmdline & comm fields.
Examples of such behavior can be found here: https://github.com/SolitudePy/linux-mal
"""
_required_framework_version = (2, 27, 0)
_version = (1, 1, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
@classmethod
def get_executable_path(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
) -> Optional[str]:
"""
Extract the executable path from task_struct.mm.exe_file
Args:
context: The context to operate on
task: task_struct object of the process
Returns:
Returns executable path or None if not available
"""
try:
mm = task.mm
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}")
return None
if not mm or not mm.is_readable():
# Kernel threads don't have mm struct
return None
try:
exe_file = mm.exe_file
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.debug(
f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}"
)
return None
if not exe_file or not exe_file.is_readable():
return None
try:
exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file)
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.debug(
f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}"
)
return None
return exe_path
@classmethod
def get_cmdline_basename(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
) -> Optional[str]:
"""
Extract the command line arguments and return the basename of the first argument.
Notes:
The read length is capped at ``MAX_ARG_STRLEN`` (32 * 4096) per the
kernel limit defined in ``include/uapi/linux/binfmts.h`` (see
linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940).
Args:
context: The context to operate on
task: task_struct object of the process
Returns:
Basename of the first command line argument or None if not available
"""
mm = task.mm
if not mm or not mm.is_readable():
return None
proc_layer_name = task.add_process_layer()
if proc_layer_name is None:
return None
start = task.mm.arg_start
size_to_read = task.mm.arg_end - task.mm.arg_start
if size_to_read <= 0:
return None
read_length = min(size_to_read, linux_constants.MAX_ARG_STRLEN)
try:
cmdline = utility.address_to_string(
context=context,
layer_name=proc_layer_name,
address=start,
count=read_length,
errors="replace",
encoding="utf-8",
)
except exceptions.InvalidAddressException as e:
vollog.debug(
f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}"
)
return None
if not cmdline:
return None
basename = PurePosixPath(cmdline).name
return basename if basename else None
@classmethod
def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]:
"""
Extract the comm field from task_struct
Args:
task: task_struct object of the process
Returns:
Process name from comm field or None if not available
"""
try:
return utility.array_to_string(task.comm)
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}")
return None
@classmethod
def extract_process_names(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]:
"""
Extract all process name sources for comparison
Returns:
Tuple of (exe_path, exe_basename, cmdline_basename, comm)
"""
exe_path = cls.get_executable_path(context, task)
exe_basename = PurePosixPath(exe_path).name if exe_path else None
if exe_basename and exe_basename.endswith(" (deleted)"):
exe_basename = exe_basename[: -len(" (deleted)")]
cmdline_basename = cls.get_cmdline_basename(context, task)
comm = cls.get_comm(task)
return exe_path, exe_basename, cmdline_basename, comm
def _detect_spoofing(
self,
exe_basename: Optional[str],
cmdline_basename: Optional[str],
comm: Optional[str],
) -> Tuple[bool, bool]:
"""
Analyze the three name sources to detect potential spoofing
Args:
exe_basename: Basename from exe_file path
cmdline_basename: Basename from command line
comm: Name from comm field
Returns:
Tuple of (cmdline_spoofed, comm_spoofed) boolean flags
"""
# Skip kernel threads - need at least 2 sources for comparison
available_sources = sum(
1 for name in [exe_basename, cmdline_basename, comm] if name
)
if available_sources < 2:
return False, False
# Check for cmdline spoofing
cmdline_spoofed = False
if exe_basename and cmdline_basename:
cmdline_spoofed = exe_basename != cmdline_basename
# Check for comm spoofing (comm is truncated to 15 characters)
comm_spoofed = False
if exe_basename and comm:
comm_spoofed = exe_basename[:15] != comm
return cmdline_spoofed, comm_spoofed
def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]:
"""
Generate process spoofing detection results
Args:
tasks: Iterator of task_struct objects
Yields:
Tuple containing process information and spoofing analysis
"""
for task in tasks:
try:
pid = task.pid
ppid = task.get_parent_pid()
exe_path, exe_basename, cmdline_basename, comm = (
self.extract_process_names(self.context, task)
)
cmdline_spoofed, comm_spoofed = self._detect_spoofing(
exe_basename, cmdline_basename, comm
)
is_deleted = exe_path.endswith(" (deleted)") if exe_path else False
# Convert None values to strings for TreeGrid compatibility
exe_path_render = exe_path if exe_path else "N/A"
cmdline_render = cmdline_basename if cmdline_basename else "N/A"
comm_render = comm if comm else "N/A"
yield (
0,
(
pid,
ppid,
exe_path_render,
cmdline_render,
comm_render,
cmdline_spoofed,
comm_spoofed,
is_deleted,
),
)
except (exceptions.InvalidAddressException, AttributeError) as e:
vollog.warning(
f"Unable to process task PID {getattr(task, 'pid', 'unknown')} at {task.vol.offset:#x}: {e}"
)
continue
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid(
[
("PID", int),
("PPID", int),
("Exe_Path", str),
("Cmdline_Basename", str),
("Comm", str),
("Cmdline_Spoofed", bool),
("Comm_Spoofed", bool),
("Exe_Deleted", bool),
],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
@@ -4,8 +4,8 @@
import logging
from typing import List
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3 import framework
import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -17,7 +17,7 @@ vollog = logging.getLogger(__name__)
class ModuleExtract(interfaces.plugins.PluginInterface):
"""Recreates an ELF file from a specific address in the kernel"""
_version = (1, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 25, 0)
framework.require_interface_version(*_required_framework_version)
@@ -37,9 +37,9 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
optional=False,
),
requirements.VersionRequirement(
name="linux_utilities_module_extract",
version=(1, 0, 0),
component=linux_utilities_module_extract.ModuleExtract,
name="linux_utilities_modules_module_extract",
version=(1, 0, 2),
component=linux_utilities_modules.ModuleExtract,
),
]
@@ -58,7 +58,7 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
module = kernel.object(object_type="module", offset=base_address, absolute=True)
elf_data = linux_utilities_module_extract.ModuleExtract.extract_module(
elf_data = linux_utilities_modules.ModuleExtract.extract_module(
self.context, self.config["kernel"], module
)
if not elf_data:
@@ -194,15 +194,11 @@ class PIDHashTable(plugins.PluginInterface):
has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type(
"pid"
).has_member(
"numbers"
) # kernels >= 2.6.24
).has_member("numbers") # kernels >= 2.6.24
has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type(
"upid"
).has_member(
"pid_chain"
) # 2.6.24 <= kernels < 4.15
).has_member("pid_chain") # 2.6.24 <= kernels < 4.15
# kernels >= 4.15
pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type(
+3 -3
View File
@@ -70,9 +70,9 @@ class Maps(plugins.PluginInterface):
def list_vmas(
cls,
task: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: True,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
True
),
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Memory Areas of a specific process.
@@ -0,0 +1,474 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
import struct
from typing import List, Set
from volatility3.framework import exceptions, constants
from volatility3.framework import renderers
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.symbols import linux
from volatility3.framework import symbols
from volatility3.plugins.linux import lsof, pslist, sockstat
from volatility3.framework.layers import scanners
from volatility3.framework.symbols.linux import network
vollog = logging.getLogger(__name__)
class Sockscan(plugins.PluginInterface):
"""Scans for network connections found in memory layer."""
_required_framework_version = (2, 6, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="SockHandlers", component=sockstat.SockHandlers, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="lsof", component=lsof.Lsof, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(4, 1, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
),
requirements.VersionRequirement(
name="linux_net", component=network.NetSymbols, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
]
def _canonicalize_symbol_addrs(
self, kernel_module_name: str, symbol_names: List[str]
) -> Set[bytes]:
"""Takes a list of symbol names and converts the address of each to the bytes
as they would appear in memory so that they can be scanned for.
Symbols that cannot be found are ignored and not included in the results.
Args:
kernel_module_name: The name of the kernel module on which to operate
symbol_names: A list of symbol names to be looked up
Returns:
A set of bytes which are the packed addresses.
"""
# get vmlinux module from context in order to build objects and read symbols
vmlinux = self.context.modules[kernel_module_name]
# get kernel layer from context so that it's dependencies can be found, and therefore scanned.
# kernel layer will be virtual and built ontop of a physical layer.
kernel_layer = self.context.layers[vmlinux.layer_name]
# detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated
# to the correct size so that they can be accurately located in the physical layer.
if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name):
pack_format = "Q" # 64 bit
else:
pack_format = "I" # 32 bit
packed_needles = set()
for symbol_name in symbol_names:
try:
needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset
except exceptions.SymbolError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Unable to find symbol {symbol_name} this will not be scanned for.",
)
continue
# use canonicalize to set the appropriate sign extension for the addr
addr = kernel_layer.canonicalize(needle_addr)
packed_addr = struct.pack(pack_format, addr)
packed_needles.add(packed_addr)
vollog.log(
constants.LOGLEVEL_VVVV,
f"Will scan for {symbol_name} using the bytes: {packed_addr.hex()}",
)
# make a warning if no symbols at all could be resolved.
if not packed_needles:
vollog.warning(
"_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information."
)
return packed_needles
def _find_memory_layer_name(self, kernel_module_name: str):
"""Find the memory layer below the kernel. Only returns a single layer,
and will warn the user if multiple layers are found.
Args:
kernel_module_name: The name of the kernel module on which to operate.
Returns:
memory_layer_name: The name of the layer below the kernel to be scanned.
"""
# get vmlinux module from context in order to build objects and read symbols
vmlinux = self.context.modules[kernel_module_name]
# get kernel layer from context so that it's dependencies can be found, and therefore scanned.
# kernel layer will be virtual and built ontop of a physical layer.
kernel_layer = self.context.layers[vmlinux.layer_name]
# TODO: Update plugin to support multiple dependencies. e.g. a memory layer and swap file.
# This is a shared problem with psscan and having a generic solution would be useful.
# Find the memory layer to scan, and provide warnings if more than one is located.
if len(kernel_layer.dependencies) > 1:
vollog.warning(
f"Kernel layer depends on multiple layers however only {kernel_layer.dependencies[0]} will be scanned by this plugin."
)
elif len(kernel_layer.dependencies) == 0:
vollog.error(
"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan."
)
raise exceptions.LayerException(
vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies"
)
memory_layer_name = kernel_layer.dependencies[0]
return memory_layer_name
def _find_file_ops_needles(self, kernel_module_name: str):
"""Retrieves socket file symbols and the offset to the 'f_op' pointer.
Args:
kernel_module_name (str): The name of the kernel module to search.
Returns:
Tuple[List[int], int]: A list of file symbol addresses and,
the offset to the 'f_op' pointer.
"""
# get vmlinux module from context in order to read symbols
vmlinux = self.context.modules[kernel_module_name]
file_ops_symbol_names = [
"socket_file_ops",
"sockfs_dentry_operations",
]
file_ops_needles = self._canonicalize_symbol_addrs(
kernel_module_name, file_ops_symbol_names
)
# get file struct to find the offset to the f_op pointer
# this is so that the file object can be created at the correct offset,
# the results of the scanner will be for the f_op member within the file
f_op_offset = vmlinux.get_type("file").relative_child_offset("f_op")
return (file_ops_needles, f_op_offset)
def _find_sk_destruct_needles(self, kernel_module_name: str):
# get vmlinux module from context in order to read symbols
vmlinux = self.context.modules[kernel_module_name]
socket_destructor_symbol_names = [
"sock_def_destruct",
"packet_sock_destruct",
"unix_sock_destructor",
"netlink_sock_destruct",
"inet_sock_destruct",
]
socket_destructor_needles = self._canonicalize_symbol_addrs(
kernel_module_name, socket_destructor_symbol_names
)
# get sock struct to find the offset to the sk_destruct pointer
# this is so that the sock object can be created at the correct offset,
# the results of the scanner will be for the sk_destruct member within the scock
sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset(
"sk_destruct"
)
return (socket_destructor_needles, sk_destruct_offset)
def _walk_file_ops_needles(
self,
kernel_module_name: str,
physical_memory_layer_name: str,
needle_addr: int,
f_op_offset: int,
):
"""
This method attempts to walk from the f_op member of files to the
corresponding socket. If sucessful the socket object is created on the
memory layer and returned.
Args:
kernel_module_name (str): The name of the kernel module from which,
to retrieve the file operations.
physical_memory_layer_name (str): The name of the physical memory layer that was scanned
needle_addr: The address of the needle that was found during the scanning
f_op_offset: The offset to the f_op member of the file type
Returns:
psock: The sock object that was built on the memory layer
"""
vmlinux = self.context.modules[kernel_module_name]
try:
# create file in the memory_layer, the native layer matches the
# kernel so that pointers can be followed
sock_physical_addr = needle_addr - f_op_offset
pfile = self.context.object(
vmlinux.symbol_table_name + constants.BANG + "file",
offset=sock_physical_addr,
layer_name=physical_memory_layer_name,
native_layer_name=vmlinux.layer_name,
)
dentry = pfile.get_dentry()
if not dentry:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping file at {hex(needle_addr)} as unable to locate dentry",
)
return None
d_inode = dentry.d_inode
if not d_inode:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry",
)
return None
socket_alloc = linux.LinuxUtilities.container_of(
d_inode, "socket_alloc", "vfs_inode", vmlinux
)
socket = socket_alloc.socket
if not (socket and socket.sk):
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid",
)
return None
# sucessfully trversed from file to sock, this will exist in the
# kernel layer, and need to be translated to the memory layer.
vsock = socket.sk.dereference()
# get virtual offset
virtual_sock_offset = vsock.vol.offset
# translate this offset to physical
native_layer = self.context.layers[vmlinux.layer_name]
physical_sock_offset, _physical_layer_name = native_layer.translate(
virtual_sock_offset
)
# build sock on the memory_layer using the physical_sock_offset
psock = self.context.object(
vmlinux.symbol_table_name + constants.BANG + "sock",
offset=physical_sock_offset,
layer_name=physical_memory_layer_name,
native_layer_name=vmlinux.layer_name,
)
return psock
except exceptions.InvalidAddressException as error:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}",
)
return None
def _extract_sock_fields(self, psock, sock_handler):
try:
sock_physical_addr = psock.vol.offset
sock_type = psock.get_type()
family = psock.get_family()
# remove results with no family
if family is None:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.",
)
return None
# TODO: invesitgate options for more invalid address handling in proccess_sock
# and the later formatting of it's results.
sock_fields = sock_handler.process_sock(psock)
# if no sock_fields we're able to be extracted then skip this result.
if not sock_fields:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.",
)
return None
sock, sock_stat, extended = sock_fields
src, src_port, dst, dst_port, state = sock_stat
protocol = sock.get_protocol()
# format results
src = renderers.NotAvailableValue() if src is None else str(src)
src_port = (
renderers.NotAvailableValue() if src_port is None else str(src_port)
)
dst = renderers.NotAvailableValue() if dst is None else str(dst)
dst_port = (
renderers.NotAvailableValue() if dst_port is None else str(dst_port)
)
state = renderers.NotAvailableValue() if state is None else str(state)
protocol = (
renderers.NotAvailableValue() if protocol is None else str(protocol)
)
# extended attributes is a dict, so this is formated to string show each
# key and value pair, seperated with a comma.
socket_filter_str = (
",".join(f"{k}={v}" for k, v in extended.items())
if extended
else renderers.NotAvailableValue()
)
# remove empty results
if (src == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue)) and (
dst == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue)
):
if state == "UNCONNECTED":
return None
elif src_port == "0" and dst_port == "0":
return None
return (
format_hints.Hex(sock_physical_addr),
family,
sock_type,
protocol,
src,
src_port,
dst,
dst_port,
state,
socket_filter_str,
)
except exceptions.InvalidAddressException as error:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}",
)
return None
def _generator(self, kernel_module_name: str):
"""Scans for sockets. Each row represents a kernel socket.
Args:
kernel_module_name: The name of the kernel module on which to operate
Yields:
addr: Physical offset
family: Socket family string (AF_UNIX, AF_INET, etc)
sock_type: Socket type string (STREAM, DGRAM, etc)
protocol: Protocol string (UDP, TCP, etc)
source addr: Source address string
source port: Source port string (not all of them are int)
destination addr: Destination address string
destination port: Destination port (not all of them are int)
state: State strings (LISTEN, CONNECTED, etc)
"""
# get vmlinux module from context in order to build objects and read symbols
vmlinux = self.context.modules[kernel_module_name]
# get the memory layer that is to be scanned.
memory_layer_name = self._find_memory_layer_name(kernel_module_name)
memory_layer = self.context.layers[memory_layer_name]
# use the init process to build a sock handler
# TODO: look into options so that sockstat.SockHandlers so that process_sock can
# be used without a task object.
init_task = vmlinux.object_from_symbol(symbol_name="init_task")
sock_handler = sockstat.SockHandlers(
self.context, kernel_module_name, init_task
)
# get progress_callback in order to use this in the scanners.
# TODO: perhaps add more detail to progress, showing method in progress and number of hits found
progress_callback = self._progress_callback
# Method 1 - find sockets by file operations, then follow pointers to sockets
file_ops_needles, f_op_offset = self._find_file_ops_needles(kernel_module_name)
# Method 2 - find sockets by socket destructor directly inside sock objects
socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles(
kernel_module_name
)
# TODO Method 3 - find sock by sk_error_report symbols
# sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen']
# this would be similar to Method 2, but using a different pointer within sock.
# add a set of seen addresses to stop possible duplication of results.
seen_sock_physical_addr = set()
# Using the calculated needles, scan the memory layer and attempt to parse the sockets located.
for needle_addr, match in memory_layer.scan(
self.context,
scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles),
progress_callback,
):
psock = None
sock_physical_addr = None
# if match is from socket_destructor_needles simply calculate the offset to the sock
if match in socket_destructor_needles:
sock_physical_addr = needle_addr - sk_destruct_offset
psock = self.context.object(
vmlinux.symbol_table_name + constants.BANG + "sock",
offset=sock_physical_addr,
layer_name=memory_layer_name,
native_layer_name=vmlinux.layer_name,
)
# if match is from file_ops_needles attempt to walk from file object to the sock
if match in file_ops_needles:
psock = self._walk_file_ops_needles(
kernel_module_name, memory_layer_name, needle_addr, f_op_offset
)
if psock is not None and sock_physical_addr not in seen_sock_physical_addr:
seen_sock_physical_addr.add(sock_physical_addr)
fields = self._extract_sock_fields(psock, sock_handler)
if fields:
yield (0, fields)
def run(self):
tree_grid_args = [
("Sock Offset", format_hints.Hex),
("Family", str),
("Type", str),
("Proto", str),
("Source Addr", str),
("Source Port", str),
("Destination Addr", str),
("Destination Port", str),
("State", str),
("Filter", str),
]
return renderers.TreeGrid(
tree_grid_args,
self._generator(self.config["kernel"]),
)
@@ -34,7 +34,9 @@ class PerfEvents(plugins.PluginInterface):
]
@classmethod
def list_perf_events(cls, context, vmlinux_module_name: str) -> Generator[
def list_perf_events(
cls, context, vmlinux_module_name: str
) -> Generator[
Tuple[
interfaces.objects.ObjectInterface,
interfaces.objects.ObjectInterface,
@@ -65,9 +65,9 @@ class Maps(interfaces.plugins.PluginInterface):
def list_vmas(
cls,
task: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: True,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
True
),
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Memory Areas of a specific process.
+3 -1
View File
@@ -49,7 +49,9 @@ class PsList(interfaces.plugins.PluginInterface):
]
@classmethod
def get_list_tasks(cls, method: str) -> Callable[
def get_list_tasks(
cls, method: str
) -> Callable[
[interfaces.context.ContextInterface, str, Callable[[int], bool]],
Iterable[interfaces.objects.ObjectInterface],
]:
@@ -52,7 +52,7 @@ class ArrowRenderer(text_renderer.CLIRenderer):
datetime.datetime: lambda: pa.timestamp("ms"),
format_hints.Bin: pa.uint64,
format_hints.Hex: pa.uint64,
format_hints.MultiTypeData: pa.utf8,
format_hints.MultiTypeData: pa.binary,
format_hints.HexBytes: pa.binary,
renderers.LayerData: pa.binary,
bytes: pa.binary,
@@ -153,6 +153,8 @@ orders the results by time."""
)
times[timestamp_type] = timestamp
self.timeline[(plugin_name, item)] = times
for plugin_name, item in self.timeline:
data.append(
(
0,
@@ -19,9 +19,7 @@ vollog = logging.getLogger(__name__)
def createservicesid(svc) -> str:
"""Calculate the Service SID"""
uni = "".join([c + "\x00" for c in svc])
sha = hashlib.sha1(
uni.upper().encode("utf-8")
).digest() # pylint: disable-msg=E1101
sha = hashlib.sha1(uni.upper().encode("utf-8")).digest() # pylint: disable-msg=E1101
dec = list()
for i in range(5):
## The use of struct here is OK. It doesn't make much sense
@@ -37,7 +37,9 @@ class PebMasquerade(interfaces.plugins.PluginInterface):
]
@classmethod
def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[
def get_process_names(
cls, proc: interfaces.objects.ObjectInterface
) -> Tuple[
Union[str, renderers.NotAvailableValue],
Union[str, renderers.NotAvailableValue],
Union[str, renderers.NotAvailableValue],
@@ -709,17 +709,15 @@ class PESymbols(interfaces.plugins.PluginInterface):
# symbol_info will be a symbol name or address requested
for symbol_info in wanted_symbols:
if (
wanted_type == wanted_names_identifier
and type(symbol_info) not in valid_name_types
if wanted_type == wanted_names_identifier and not isinstance(
symbol_info, tuple(valid_name_types)
):
raise ValueError(
f"The requested symbol name has a type of {type(symbol_info)} which is not in the allowed set of {valid_name_types}"
)
elif (
wanted_type == wanted_addresses_identifier
and type(symbol_info) not in valid_address_types
elif wanted_type == wanted_addresses_identifier and not isinstance(
symbol_info, tuple(valid_address_types)
):
raise ValueError(
f"The requested address has a type of {type(symbol_info)} which is not in the allowed set of {valid_address_types}"
@@ -167,7 +167,8 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
Filter function for passing to the `list_processes` method
"""
return lambda x: not (
return lambda x: (
not (
x.is_valid()
and x.ActiveThreads > 0
and x.UniqueProcessId != 4
@@ -175,6 +176,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
and x.ExitTime.QuadPart == 0
and x.get_handle_count() != renderers.UnreadableValue()
)
)
@classmethod
def create_name_filter(
@@ -214,9 +216,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
False
),
) -> Iterator["extensions.EPROCESS"]:
"""Lists all the processes in the given layer that are in the pid
config option.
@@ -150,9 +150,9 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
False
),
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Scans for processes using the poolscanner module and constraints.
@@ -53,9 +53,9 @@ class PsTree(interfaces.plugins.PluginInterface):
def find_level(
self,
pid: int,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
False
),
) -> None:
"""Finds how deep the pid is in the processes list."""
seen = {pid}
@@ -77,9 +77,9 @@ class PsTree(interfaces.plugins.PluginInterface):
def _generator(
self,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
False
),
):
"""Generates the Tree of processes."""
kernel = self.context.modules[self.config["kernel"]]
@@ -5,8 +5,13 @@ import logging
from struct import unpack
from typing import Tuple
try:
from Crypto.Cipher import ARC4, AES
from Crypto.Hash import HMAC
except ImportError:
# Debian/Ubuntu ship pycryptodome under Cryptodome namespace
from Cryptodome.Cipher import ARC4, AES
from Cryptodome.Hash import HMAC
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
@@ -7,7 +7,11 @@ import logging
from struct import pack, unpack
from typing import List, Optional, Tuple
from Crypto.Cipher import AES, ARC4, DES
try:
from Crypto.Cipher import ARC4, DES, AES
except ImportError:
# Debian/Ubuntu ship pycryptodome under Cryptodome namespace
from Cryptodome.Cipher import ARC4, DES, AES
from volatility3.framework import interfaces, renderers, exceptions, constants
from volatility3.framework.configuration import requirements
@@ -6,7 +6,11 @@ from struct import unpack
from typing import Optional
import hashlib
try:
from Crypto.Cipher import ARC4, DES, AES
except ImportError:
# Debian/Ubuntu ship pycryptodome under Cryptodome namespace
from Cryptodome.Cipher import ARC4, DES, AES
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
@@ -99,12 +99,8 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
thread_tid = ethread.Cid.UniqueThread
thread_start_addr = ethread.StartAddress
thread_win32start_addr = ethread.Win32StartAddress
thread_create_time = (
ethread.get_create_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_exit_time = (
ethread.get_exit_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_create_time = ethread.get_create_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_exit_time = ethread.get_exit_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
owner_proc = None
if vads_cache is not None:
@@ -115,9 +115,9 @@ class VadInfo(interfaces.plugins.PluginInterface):
def list_vads(
cls,
proc: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: (
False
),
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Address Descriptors of a specific process.
@@ -198,7 +198,9 @@ class VadInfo(interfaces.plugins.PluginInterface):
return file_handle
def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[
def _generator(
self, procs: List[interfaces.objects.ObjectInterface]
) -> Generator[
Tuple[
int,
Tuple[
@@ -52,7 +52,6 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("idr", extensions.IDR)
self.set_type_class("address_space", extensions.address_space)
self.set_type_class("page", extensions.page)
self.set_type_class("module_sect_attr", extensions.module_sect_attr)
# Might not exist in the current symbols
self.optional_set_type_class("module", extensions.module)
@@ -61,6 +60,8 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t)
self.optional_set_type_class("scatterlist", extensions.scatterlist)
self.optional_set_type_class("module_sect_attr", extensions.module_sect_attr)
self.optional_set_type_class("bin_attribute", extensions.bin_attribute)
# kernels >= 4.18
self.optional_set_type_class("timespec64", extensions.timespec64)
@@ -179,25 +179,45 @@ class module(generic.GenericIntelProcess):
return None
def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int:
"""Try to determine the number of valid sections"""
symbol_table_name = self.get_symbol_table_name()
arr = self._context.object(
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=grp.attrs,
subtype=self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "pointer"
),
count=25,
)
"""Try to determine the number of valid sections. Support for kernels > 6.14-rc1.
idx = 0
while arr[idx] and arr[idx].is_readable():
idx = idx + 1
return idx
Resources:
- https://github.com/torvalds/linux/commit/d8959b947a8dfab1047c6fd5e982808f65717bfe
- https://github.com/torvalds/linux/commit/e0349c46cb4fbbb507fa34476bd70f9c82bad359
"""
if grp.has_member("bin_attrs"):
arr_offset_ptr = grp.bin_attrs
arr_subtype = "bin_attribute"
else:
arr_offset_ptr = grp.attrs
arr_subtype = "attribute"
if not arr_offset_ptr.is_readable():
vollog.log(
constants.LOGLEVEL_V,
f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}",
)
return 0
# We chose 100 as an arbitrary guard value to prevent
# looping forever in extreme cases, and because 100 is not expected
# to be a valid number of sections. If that still happens,
# Vol3 module processing will indicate that it is missing information
# with the following message:
# "Unable to reconstruct the ELF for module struct at"
# See PR #1773 for more information.
bin_attrs_list = utility.dynamically_sized_array_of_pointers(
context=self._context,
array=arr_offset_ptr.dereference(),
subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype,
iterator_guard_value=100,
)
return len(bin_attrs_list)
@functools.cached_property
def number_of_sections(self) -> int:
# Dropped in 6.14-rc1: d8959b947a8dfab1047c6fd5e982808f65717bfe
if self.sect_attrs.has_member("nsections"):
return self.sect_attrs.nsections
@@ -205,15 +225,18 @@ class module(generic.GenericIntelProcess):
def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Get a list of section attributes for the given module."""
if self.number_of_sections == 0:
vollog.debug(
f"Invalid number of sections ({self.number_of_sections}) for module at offset {self.vol.offset:#x}"
)
return []
symbol_table_name = self.get_symbol_table_name()
arr = self._context.object(
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=self.sect_attrs.attrs.vol.offset,
subtype=self._context.symbol_space.get_type(
symbol_table_name + constants.BANG + "module_sect_attr"
),
subtype=self.sect_attrs.attrs.vol.subtype,
count=self.number_of_sections,
)
@@ -1092,14 +1115,11 @@ class mm_struct(objects.StructType):
vm_area_struct objects
"""
for vma in self._do_get_vma_iter():
try:
vma.vm_start
vma.vm_end
vma.get_protection()
if not vma.is_valid():
vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}")
continue
yield vma
except exceptions.InvalidAddressException:
vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}")
class super_block(objects.StructType):
@@ -1235,6 +1255,39 @@ class vm_area_struct(objects.StructType):
retval = retval + "-"
return retval
def is_valid(self) -> bool:
"""Validate a VMA struct to prevent processing smeared entries."""
try:
start = self.vm_start
end = self.vm_end
self.get_protection()
except exceptions.InvalidAddressException:
return False
layer = self._context.layers[self.vol.layer_name]
length = end - start
if (
(start > end)
or (start == 0 and length == 0)
or (length % layer.page_size != 0)
):
return False
if self.vm_file != 0:
try:
inode = self.vm_file.get_inode()
except exceptions.InvalidAddressException:
return False
# Verify that a file-backed VMA's page offset
# is not greater than the size of the file's inode.
# Check only inode sizes greater than 0 to account for
# special devices (e.g. "/dev/dri/card0") and prevent false negatives.
if inode.i_size > 0 and self.get_page_offset() > inode.i_size:
return False
return True
# only parse the rwx bits
def get_protection(self) -> str:
return self._parse_flags(self.vm_flags & 0b1111, vm_area_struct.perm_flags)
@@ -3158,7 +3211,9 @@ class module_sect_attr(objects.StructType):
"""
if hasattr(self, "battr"):
try:
return utility.pointer_to_string(self.battr.attr.name, count=32)
return utility.pointer_to_string(
self.battr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# if battr is present then its name attribute needs to be valid
vollog.debug(f"Invalid battr name for section at {self.vol.offset:#x}")
@@ -3166,14 +3221,18 @@ class module_sect_attr(objects.StructType):
elif self.name.vol.type_name == "array":
try:
return utility.array_to_string(self.name, count=32)
return utility.array_to_string(
self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# specifically do not return here to give `mattr` a chance
vollog.debug(f"Invalid direct name for section at {self.vol.offset:#x}")
elif self.name.vol.type_name == "pointer":
try:
return utility.pointer_to_string(self.name, count=32)
return utility.pointer_to_string(
self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
# specifically do not return here to give `mattr` a chance
vollog.debug(
@@ -3183,10 +3242,33 @@ class module_sect_attr(objects.StructType):
# if everything else failed...
if hasattr(self, "mattr"):
try:
return utility.pointer_to_string(self.mattr.attr.name, count=32)
return utility.pointer_to_string(
self.mattr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
vollog.debug(
f"Unresolvable name for for section at {self.vol.offset:#x}"
)
return None
class bin_attribute(objects.StructType):
def get_name(self) -> Optional[str]:
"""
Performs extraction of the bin_attribute name
"""
try:
return utility.pointer_to_string(
self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE
)
except exceptions.InvalidAddressException:
vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}")
return None
@property
def address(self) -> int:
"""Equivalent to module_sect_attr.address:
- https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f
"""
return self.private
@@ -11,11 +11,7 @@ from typing import (
)
from volatility3 import framework
from volatility3.framework import (
interfaces,
exceptions,
symbols,
)
from volatility3.framework import interfaces, exceptions, symbols, deprecation
from volatility3.framework.constants import linux as linux_constants
from volatility3.framework.symbols.linux import extensions
@@ -35,54 +31,20 @@ vollog = logging.getLogger(__name__)
# ModuleExtract.extract_module is the entry point and only visible method for plugins
# See PR #1773
@deprecation.renamed_class(
deprecated_class_name="ModuleExtract",
removal_date="2026-06-01",
message="volatility3.framework.symbols.linux.utilities.module_extract.ModuleExtract is to be deprecated. Use volatility3.framework.symbols.linux.utilities.modules.ModuleExtract instead.",
)
class ModuleExtract(interfaces.configuration.VersionableInterface):
"""Extracts Linux kernel module structures into an analyzable ELF file"""
_version = (1, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 25, 0)
framework.require_interface_version(*_required_framework_version)
@classmethod
def _get_module_section_count(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
grp: interfaces.objects.ObjectInterface,
) -> int:
"""
Used to manually determine the section count for kernels that do not track
this count directly within the attribute structures
"""
kernel = context.modules[vmlinux_name]
count = 0
try:
array = kernel.object(
object_type="array",
offset=grp.attrs,
sub_type=kernel.get_type("pointer"),
count=50,
absolute=True,
)
# Walk up to 50 sections counting until we reach the end or a page fault
for sect in array:
if sect.vol.offset == 0:
break
count += 1
except exceptions.InvalidAddressException:
# Use whatever count we reached before the error
vollog.debug(
f"Exception hit counting sections for module at {module.vol.offset:#x}"
)
return count
@classmethod
def _find_section(
cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int
@@ -261,54 +223,6 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
return sym_table_data
@classmethod
def _enumerate_original_sections(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
) -> Optional[Dict[int, str]]:
"""
Enumerates the module's sections as maintained by the kernel after load time
'Early' sections like .init.text and .init.data are discarded after module
initialization, so they are not expected to be in memory during extraction
"""
if hasattr(module.sect_attrs, "nsections"):
num_sections = module.sect_attrs.nsections
else:
num_sections = cls._get_module_section_count(
context, vmlinux_name, module.sect_attrs.grp
)
if num_sections > 1024 or num_sections == 0:
vollog.debug(
f"Invalid number of sections ({num_sections}) for module at offset {module.vol.offset:#x}"
)
return None
vmlinux = context.modules[vmlinux_name]
# This is declared as a zero sized array, so we create ourselves
attribute_type = module.sect_attrs.attrs.vol.subtype
sect_array = vmlinux.object(
object_type="array",
subtype=attribute_type,
offset=module.sect_attrs.attrs.vol.offset,
count=num_sections,
absolute=True,
)
sections: Dict[int, str] = {}
# for each section, gather its name and address
for index, section in enumerate(sect_array):
name = section.get_name()
sections[section.address] = name
return sections
@classmethod
def _parse_sections(
cls,
@@ -325,10 +239,12 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
The data of .strtab is read directly off the module structure and not its section
as the section from the original module has no meaning after loading as the kernel does not reference it.
"""
original_sections = cls._enumerate_original_sections(
context, vmlinux_name, module
)
if original_sections is None:
original_sections = {}
for index, section in enumerate(module.get_sections()):
name = section.get_name()
original_sections[section.address] = name
if not original_sections:
return None
kernel = context.modules[vmlinux_name]
@@ -702,9 +618,10 @@ class ModuleExtract(interfaces.configuration.VersionableInterface):
return None
# Gather sections
updated_sections, strtab_index, symtab_index = cls._parse_sections(
context, vmlinux_name, module
)
parse_sections_result = cls._parse_sections(context, vmlinux_name, module)
if parse_sections_result is None:
return None
updated_sections, strtab_index, symtab_index = parse_sections_result
kernel = context.modules[vmlinux_name]
@@ -1,5 +1,7 @@
import logging
import warnings
import functools
import struct
from abc import ABCMeta, abstractmethod
from typing import (
Callable,
@@ -15,7 +17,6 @@ from typing import (
Union,
)
import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract
from volatility3 import framework
from volatility3.framework import (
constants,
@@ -24,12 +25,14 @@ from volatility3.framework import (
interfaces,
objects,
renderers,
symbols,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.symbols.linux.utilities import tainting
from volatility3.framework.constants import linux as linux_constants
vollog = logging.getLogger(__name__)
@@ -71,7 +74,7 @@ class ModuleGathererInterface(
class Modules(interfaces.configuration.VersionableInterface):
"""Kernel modules related utilities."""
_version = (3, 0, 1)
_version = (3, 0, 2)
_required_framework_version = (2, 0, 0)
framework.require_interface_version(*_required_framework_version)
@@ -311,6 +314,7 @@ class Modules(interfaces.configuration.VersionableInterface):
return run_results
@staticmethod
@functools.lru_cache
def get_modules_memory_boundaries(
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
@@ -781,6 +785,740 @@ class Modules(interfaces.configuration.VersionableInterface):
yield name, value
# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory
# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time
# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections
# There are also several other significant complications that we must deal with when trying to extract an LKM
# that can be analyzed with static analysis tools
# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections
# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense
# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory
# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this,
# we create the .shstrtab based on the sections in memory and then glue it in as the final section
# ModuleExtract.extract_module is the entry point and only visible method for plugins
class ModuleExtract(interfaces.configuration.VersionableInterface):
"""Extracts Linux kernel module structures into an analyzable ELF file"""
_version = (1, 0, 2)
_required_framework_version = (2, 25, 0)
framework.require_interface_version(*_required_framework_version)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="linux_utilities_modules_modules",
component=Modules,
version=(3, 0, 2),
),
]
@classmethod
def _find_section(
cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int
) -> Optional[Tuple[str, int, int, int]]:
"""
Finds the section containing `sym_address`
"""
for name, index, address, size in section_lookups:
if address <= sym_address < address + size:
return name, index, address, size
return None
@classmethod
def _get_st_info_for_sym(
cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str
) -> bytes:
"""
This is a helper function called from `_fix_sym_table`
Calculates the `st_info` value for the given symbol
Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html
"""
if sym.st_name > 0:
# Global symbol
bind = linux_constants.STB_GLOBAL
if sym_address == 0:
sect_type = linux_constants.STT_NOTYPE
elif sect_name:
# rela = relocations
if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1:
sect_type = linux_constants.STT_FUNC
else:
sect_type = linux_constants.STT_OBJECT
else:
# outside the module being extracted
sect_type = linux_constants.STT_NOTYPE
else:
# Local symbol
bind = linux_constants.STB_LOCAL
sect_type = linux_constants.STT_SECTION
# Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO
bind_bits = (bind << 4) & 0xF0
type_bits = sect_type & 0xF
st_info_int = (bind_bits | type_bits) & 0xFF
return struct.pack("B", st_info_int)
@classmethod
def _get_fixed_sym_fields(
cls,
st_fmt: str,
sym: interfaces.objects.ObjectInterface,
sections: List[Tuple[str, int, int, int]],
) -> Tuple[str, int, int, int]:
"""
This is a helper function called from `_fix_sym_table`
The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading
Static analysis tools do not understand these transformed values as they only make sense to the kernel loader
We must de-mangle these to have analysis tools understand symbols (a key aspect)
"""
# Start by trying to map a symbol to its section
sym_address = sym.st_value
sect_info = cls._find_section(sections, sym_address)
if not sect_info:
# Symbol does not point into the module being extracted
sect_name, sect_index, sect_address = None, None, None
st_value_int = sym_address
else:
# relative address inside the section
sect_name, sect_index, sect_address, _ = sect_info
st_value_int = sym_address - sect_address
# Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file
# formatted to be written into the extracted file
st_value = struct.pack(st_fmt, st_value_int)
# returns formatted to be written into the extracted file
st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name)
# format to reference its section, if any
if sect_name:
st_shndx = struct.pack("<H", sect_index)
else:
st_shndx = struct.pack("<H", sym.st_shndx)
return sect_name, st_value, st_info, st_shndx
@classmethod
def _fix_sym_table(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
original_sections: Dict[int, str],
section_sizes: Dict[int, int],
sym_type_name: str,
st_fmt: str,
module: extensions.module,
) -> Optional[bytes]:
"""
Args:
context: The context on which to operate.
vmlinux_name: The name of the kernel module.
original_sections: Dict of module section addresses and names.
section_sizes: Dict of module section addresses and sizes.
sym_type_name: ELF symbol type name (should be one of "Elf64_Sym" or "Elf32_Sym").
st_fmt: "struct"-like unpack format string (should be one of "<Q" or "<I").
module: The Linux "module" object we're currently parsing.
This function implements the most painful part of the reconstruction
The symbols in .symtab are broken/mangled during loading.
We need to normalize these for static analysis tools to understand the references.
Without proper symbols, analysis is pretty pointless and gets nowhere.
Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html
"""
kernel = context.modules[vmlinux_name]
# Gather the section information into a list
section_lookups: List[Tuple[str, int, int, int]] = []
for index, (address, name) in enumerate(original_sections.items()):
# We are fixing symtab references...
if name == ".symtab":
continue
size = section_sizes[address]
# Add 1 to account for leading NULL section
section_lookups.append((name, index + 1, address, size))
# Build the array of symbols as they are in memory
sym_type = kernel.get_type(sym_type_name)
symbols = kernel.object(
object_type="array",
subtype=sym_type,
offset=module.section_symtab,
count=module.num_symtab,
absolute=True,
)
# used to hold the new (fixed) symbol table
sym_table_data = b""
# build a correct/normalized Elf32_Sym or Elf64_Sym for each symbol
for sym in symbols:
# get the mangled fields' correct values
sect_name, st_value, st_info, st_shndx = cls._get_fixed_sym_fields(
st_fmt, sym, section_lookups
)
# these aren't mangled during loading
st_name = struct.pack("<I", sym.st_name)
st_other = struct.pack("B", sym.st_other)
st_size = struct.pack(st_fmt, sym.st_size)
# The order as in the ELF specification. The order is not the same between 32 and 64 bit symbols
if st_fmt == "<I":
sym_data = st_name + st_value + st_size + st_info + st_other + st_shndx
else:
sym_data = st_name + st_info + st_other + st_shndx + st_value + st_size
# This should never happen regardless of smear or other issues in the data. We build the structure to spec.
if len(sym_data) != sym_type.size:
vollog.error(
f"Size of sym_data is {len(sym_data)} expected {sym_type.size} for symbol at value {sym.st_value} in section {sect_name}"
)
return None
# add the symbol's data to the overall symbol table
sym_table_data += sym_data
if len(sym_table_data) == 0:
sym_table_data = None
return sym_table_data
@classmethod
def _parse_sections(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
) -> Optional[Tuple[List, int, int]]:
"""
This function first parses the sections as maintained by the kernel
It then orders the sections by load address, and then gathers the data of each section
We also track the file_offset to correctly have alignment in the output file
.symtab requires special handling as its so broken in memory as described in `_fix_sym_table`
The data of .strtab is read directly off the module structure and not its section
as the section from the original module has no meaning after loading as the kernel does not reference it.
"""
kernel = context.modules[vmlinux_name]
kernel_layer = context.layers[kernel.layer_name]
modules_addr_min, modules_addr_max = Modules.get_modules_memory_boundaries(
context, vmlinux_name
)
modules_addr_min &= kernel_layer.address_mask
modules_addr_max &= kernel_layer.address_mask
original_sections = {}
for index, section in enumerate(module.get_sections()):
# Extra sanity check, to prevent OOM on heavily smeared samples at line
# "size = next_address - address"
if not (
modules_addr_min
<= section.address & kernel_layer.address_mask
< modules_addr_max
):
continue
name = section.get_name()
original_sections[section.address] = name
if not original_sections:
return None
if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name):
sym_type = "Elf64_Sym"
elf_hdr_type = "Elf64_Ehdr"
st_fmt = "<Q"
else:
sym_type = "Elf32_Sym"
elf_hdr_type = "Elf32_Ehdr"
st_fmt = "<I"
# At this point, we have the sections starting addresses and names,
# but the kernel does not track the size
# To recover the size, we sort by address and then use the next section as the boundary to calculate size
# .symtab (the symbol table) and .strtab (the strings table) require special handling.
# All others can be read with padding
# get the addresses in sorted order, can index into `original_sections` for names
sorted_addresses = sorted(original_sections.keys())
# We need to track where .symtab is for symbol name offsets
symtab_address = None
strtab_index = None
# Section data starts after the file header
file_offset = kernel.get_type(elf_hdr_type).vol.size
# The ordered set of sections along with their fixed data
updated_sections: List[Tuple[str, int, int, bytes]] = []
# A mapping of section start addresses to sizes
# original_sections does not have this information for reasons explained above
section_sizes: Dict[int, int] = {}
for index, address in enumerate(sorted_addresses):
sect_name = original_sections[address]
# Read out the string table. The full size is not kept, so we give each symbol's string up to 256 bytes
if sect_name == ".strtab":
# Read out symbol strings, giving up to 256 bytes per symbol
data = kernel_layer.read(
module.section_strtab, module.num_symtab * 256, pad=True
)
# The string table should end with two NULLs, but the kernel does not enforce this
end_index = data.find(b"\x00\x00")
if end_index != -1:
data = data[: end_index + 1]
strtab_index = index
# The symbol table in memory is completely transformed and broken from how it appears on disk
# We need to process it last to fix the symbol table entries back to their correct values
elif sect_name == ".symtab":
symtab_address = address
continue
else:
# Compute based on the boundary of the next address-sorted section
try:
# Get the next section in order
next_address = sorted_addresses[index + 1]
size = next_address - address
except IndexError:
## We are at the last section so we need to pick a size
size = 0x10000
vollog.debug(f"Defaulting section {sect_name} to size {size:#x}")
# Read the section normally..
data = kernel_layer.read(address, size, pad=True)
# store the section information in order
updated_sections.append((sect_name, address, file_offset, data))
# Track sizes of each section
section_sizes[address] = len(data)
file_offset += len(data)
if symtab_address:
# Perform the painful demangling of symbol table structures
data = cls._fix_sym_table(
context,
vmlinux_name,
original_sections,
section_sizes,
sym_type,
st_fmt,
module,
)
if not data:
vollog.debug(
f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover."
)
return None
symtab_index = len(updated_sections)
# Manually add symtab with the correct data
updated_sections.append((".symtab", symtab_address, file_offset, data))
else:
vollog.debug(
f"Did not find a .symtab section for module at {module.vol.offset:#x}. Cannot recover."
)
return None
return updated_sections, strtab_index, symtab_index
@classmethod
def _make_elf_header(
cls, bits: int, sect_hdr_offset: int, num_sections: int
) -> Optional[bytes]:
"""
Creates a `bits` bit ELF header for the file based on recovered values
Called last as it needs information computed from the sections
Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
if bits == 32:
fmt = "<I"
e_ident = (
b"\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
e_machine_int = 3 # EM_X86_86
e_ehsize_int = 52
e_shentsize_int = 40
header_size = 52
else:
fmt = "<Q"
e_ident = (
b"\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
e_machine_int = 0x3E # EM_X86_64
e_ehsize_int = 64
e_shentsize_int = 64
header_size = 64
e_type = struct.pack("<H", 1) # relocatable
e_machine = struct.pack("<H", e_machine_int)
e_version = struct.pack("<I", 1)
e_entry = b"\x00" * int(
bits / 8
) # The .init sections are freed after module load
e_phoff = b"\x00" * int(bits / 8) # No program headers
e_shoff = struct.pack(fmt, sect_hdr_offset)
e_flags = b"\x00\x00\x00\x00"
e_ehsize = struct.pack("<H", e_ehsize_int)
e_phentsize = b"\x00\x00"
e_phnum = b"\x00\x00"
e_shentsize = struct.pack("<H", e_shentsize_int)
e_shnum = struct.pack("<H", num_sections + 1)
e_shstrndx = struct.pack("<H", num_sections)
header = (
e_ident
+ e_type
+ e_machine
+ e_version
+ e_entry
+ e_phoff
+ e_shoff
+ e_flags
+ e_ehsize
+ e_phentsize
+ e_phnum
+ e_shentsize
+ e_shnum
+ e_shstrndx
)
# should never happen as we make the header ourselves
if len(header) != header_size:
vollog.error(
f"Making Elf header for arch {bits} created a header of {len(header)} bytes. Cannot proceed"
)
return None
return header
@classmethod
def _calc_sect_type(cls, section_name: str) -> Optional[int]:
"""
This function makes a best effort to map common section names
to their attributes
"""
known_sections = {
".note.gnu.build-id": linux_constants.SHT_NOTE,
".text": linux_constants.SHT_PROGBITS,
".init.text": linux_constants.SHT_PROGBITS,
".exit.text": linux_constants.SHT_PROGBITS,
".static_call.text": linux_constants.SHT_PROGBITS,
".rodata": linux_constants.SHT_PROGBITS,
".modinfo": linux_constants.SHT_PROGBITS,
"__param": linux_constants.SHT_PROGBITS,
".data": linux_constants.SHT_PROGBITS,
".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS,
".comment": linux_constants.SHT_PROGBITS,
".shstrtab": linux_constants.SHT_STRTAB,
".symtab": linux_constants.SHT_SYMTAB,
".strtab": linux_constants.SHT_STRTAB,
}
sect_type_val = linux_constants.SHT_PROGBITS
if section_name.find(".rela.") != -1:
sect_type_val = linux_constants.SHT_RELA
elif section_name in known_sections:
sect_type_val = known_sections[section_name]
return sect_type_val
# all sections from memory are allocated (SHF_ALLOC)
# special check certain other sections to try and ensure extra flags are added where needed
@classmethod
def _calc_sect_flags(cls, name: str) -> int:
"""
Make a best effort to map common section names to their permissions
If we miss a section here, users of common static analysis tools can mark the
sections are writable or executable manually, but that becomes very cumbersome
and breaks initial analysis by the tool
"""
# All sections in memory are allocated (`A` in readelf -S)
flags = linux_constants.SHF_ALLOC
if name in [".text", ".init.text", ".exit.text", ".static_call.text"]:
flags = flags | linux_constants.SHF_EXECINSTR
elif name in [
".data",
".init.data",
".exit.data",
".bss",
"__tracepoints",
".data.once",
"_ftrace_events",
".gnu.linkonce.this_module",
]:
flags = flags | linux_constants.SHF_WRITE
return flags
@classmethod
def _calc_link(
cls, name: str, strtab_index: int, symtab_index: int, sect_type: int
) -> int:
"""
Calculates the link value for a section
The most important ones are symtab indexes for relocations
and to point the symbol table to the string tab
Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html
"""
# looking for RELA sections
if name.find(".rela.") != -1:
return symtab_index
# per spec: "The section header index of the associated string table."
elif sect_type == linux_constants.SHT_SYMTAB:
return strtab_index
return 0
@classmethod
def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int:
"""
Calculates the entsize for relocation sections and the symbol table section
Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html
"""
# looking for RELA sections
if name.find(".rela.") != -1:
return 24
# per spec: "The section header index of the associated string table."
elif sect_type == linux_constants.SHT_SYMTAB:
if bits == 32:
return 16
else:
return 24
return 0
@classmethod
def _make_section_header(
cls,
bits: int,
name_index: int,
name: str,
address: int,
size: int,
file_offset: int,
strtab_index: int,
symtab_index: int,
) -> Optional[bytes]:
"""
Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section
"""
if bits == 32:
fmt = "<I"
sect_size = 40
else:
fmt = "<Q"
sect_size = 64
sect_header_type_int = cls._calc_sect_type(name)
flags = cls._calc_sect_flags(name)
link = cls._calc_link(name, strtab_index, symtab_index, sect_header_type_int)
entsize = cls._calc_entsize(name, sect_header_type_int, bits)
try:
sh_name = struct.pack("<I", name_index)
sh_type = struct.pack("<I", sect_header_type_int)
sh_flags = struct.pack(fmt, flags)
sh_addr = struct.pack(fmt, address)
sh_offset = struct.pack(fmt, file_offset)
sh_size = struct.pack(fmt, size)
sh_link = struct.pack("<I", link)
sh_info = b"\x00" * 4
sh_addralign = struct.pack(fmt, 1)
sh_entsize = struct.pack(fmt, entsize)
# catch overflows of offset/address/size
except struct.error:
vollog.debug(
f"Unable to build section header for section {name} at address {address:#x}"
)
return None
data = (
sh_name
+ sh_type
+ sh_flags
+ sh_addr
+ sh_offset
+ sh_size
+ sh_link
+ sh_info
+ sh_addralign
+ sh_entsize
)
# This should never happen regardless of smear or other issues in the data. We build the structure to spec.
if len(data) != sect_size:
vollog.error(
f"Size of section data is {len(data)} expected {sect_size} for section {name} at address {address:#x}"
)
return None
return data
@classmethod
def extract_module(
cls,
context: interfaces.context.ContextInterface,
vmlinux_name: str,
module: extensions.module,
) -> Optional[bytes]:
# Bail early if bad address sent in
try:
hasattr(module.sect_attrs, "nsections")
except exceptions.InvalidAddressException:
vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.")
return None
# Gather sections
parse_sections_result = cls._parse_sections(context, vmlinux_name, module)
if parse_sections_result is None:
return None
updated_sections, strtab_index, symtab_index = parse_sections_result
kernel = context.modules[vmlinux_name]
# Figure out header sizes
if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name):
header_type = "Elf64_Ehdr"
section_type = "Elf64_Shdr"
bits = 64
else:
header_type = "Elf32_Ehdr"
section_type = "Elf32_Shdr"
bits = 32
header_type_size = kernel.get_type(header_type).size
section_type_size = kernel.get_type(section_type).size
# Per Linux-spec, all LKMs must start with a null section header
# This buffer is used to hold the headers as they are built
sections_headers = b"\x00" * section_type_size
# Holder of the data of the sections
sections_data = b""
# the .shstrtab section is "\x00" + section name for each section
# followed by a terminating null.
# It starts with the null string (\x00)
shstrtab_data = b"\x00"
# Track where we end the sections and data to glue `.shstrtab` after
last_file_offset = None
last_sect_size = None
# Start at 1 in the string table
name_index = 1
# Create the actual section headers
for index, (name, address, file_offset, section_data) in enumerate(
updated_sections
):
# Make the section header
header_bytes = cls._make_section_header(
bits,
name_index,
name,
address,
len(section_data),
file_offset,
strtab_index,
symtab_index,
)
if not header_bytes:
vollog.debug(f"make_section_header failed for section {name}")
return None
# ndex into the string table
name_index += len(name) + 1
# concatenate the header and section bytes
sections_headers += header_bytes
sections_data += section_data
# track where we are so .shstrtab goes into correct offset
last_file_offset = file_offset
last_sect_size = len(section_data)
# append each section name to what will become .shstrtab
shstrtab_data += bytes(name, encoding="utf8") + b"\x00"
# stick our own section reference string at end
# name_index points to the end of the last section string after the loop ends
shstrtab_data += b".shstrtab\x00"
# create our .shstrtab section so sections have names
sections_headers += cls._make_section_header(
bits,
name_index,
".shstrtab",
0,
len(shstrtab_data),
last_file_offset + last_sect_size,
strtab_index,
symtab_index,
)
sections_data += shstrtab_data
num_sections = len(updated_sections) + 1
header = cls._make_elf_header(
bits,
header_type_size + len(sections_data),
num_sections,
)
if not header:
vollog.error(
f"Hit error creating Elf header for module at {module.vol.offset:#x}"
)
return None
# Return our beautiful, hand-crafted, farm raised ELF file
return header + sections_data + sections_headers
class ModuleGathererLsmod(ModuleGathererInterface):
"""
Gathers modules from the main kernel list
@@ -976,7 +1714,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface):
file_name = renderers.NotApplicableValue()
if dump and open_implementation:
elf_data = linux_utilities_module_extract.ModuleExtract.extract_module(
elf_data = ModuleExtract.extract_module(
context, kernel_module_name, module
)
if not elf_data:
@@ -126,8 +126,7 @@ class POOL_HEADER(objects.StructType):
infomask_value = infomask_data[addr + infomask_offset]
pointercount_value = int.from_bytes(
infomask_data[
addr
+ pointercount_offset : addr
addr + pointercount_offset : addr
+ pointercount_offset
+ pointercount_size
],
@@ -165,8 +164,7 @@ class POOL_HEADER(objects.StructType):
(padding_length,) = struct.unpack(
"<I",
infomask_data[
addr
- optional_headers_length : addr
addr - optional_headers_length : addr
- optional_headers_length
+ 4
],
@@ -289,7 +289,7 @@ class PDBUtility(interfaces.configuration.VersionableInterface):
)
break
except PermissionError:
vollog.warning(
vollog.debug(
f"Cannot write necessary symbol file, please check permissions on {potential_output_filename}"
)
continue
@@ -525,6 +525,10 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface):
.. note:: The pdb_names must be a list of byte strings, unicode strs will not match against the data scanned
"""
_version = (1, 0, 0)
_required_framework_version = (2, 27, 0)
overlap = 0x4000
"""The size of overlap needed for the signature to ensure data cannot hide between two scanned chunks"""
thread_safe = True
@@ -548,9 +552,7 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface):
)
for match in re.finditer(pattern, data, flags=re.DOTALL):
pdb_name = data[
match.start(0)
+ 4
+ self._RSDS_format.size : match.start(0)
match.start(0) + 4 + self._RSDS_format.size : match.start(0)
+ len(match.group())
- 1
]