mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge pull request #1977 from volatilityfoundation/release/v2.28.0
Release/v2.28.0
This commit is contained in:
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
max_ptr = max(
|
||||
max_ptr,
|
||||
(pointer ^ (pointer & 0xFFF))
|
||||
% test.layer_type.maximum_address,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
table = self._context.layers.read(
|
||||
self._base_layer, base_address, self.page_size
|
||||
)
|
||||
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 isinstance(excp, exceptions.PagedInvalidAddressException):
|
||||
mask = (1 << excp.invalid_bits) - 1
|
||||
else:
|
||||
mask = (1 << self._page_size_in_bits) - 1
|
||||
length_diff = mask + 1 - (offset & mask)
|
||||
if skip_mask is None:
|
||||
# We can jump more if we know where the page fault occured
|
||||
if isinstance(excp, exceptions.PagedInvalidAddressException):
|
||||
skip_mask = (1 << excp.invalid_bits) - 1
|
||||
else:
|
||||
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),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,13 +167,15 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
Filter function for passing to the `list_processes` method
|
||||
"""
|
||||
|
||||
return lambda x: not (
|
||||
x.is_valid()
|
||||
and x.ActiveThreads > 0
|
||||
and x.UniqueProcessId != 4
|
||||
and x.InheritedFromUniqueProcessId != 4
|
||||
and x.ExitTime.QuadPart == 0
|
||||
and x.get_handle_count() != renderers.UnreadableValue()
|
||||
return lambda x: (
|
||||
not (
|
||||
x.is_valid()
|
||||
and x.ActiveThreads > 0
|
||||
and x.UniqueProcessId != 4
|
||||
and x.InheritedFromUniqueProcessId != 4
|
||||
and x.ExitTime.QuadPart == 0
|
||||
and x.get_handle_count() != renderers.UnreadableValue()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -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
|
||||
|
||||
from Crypto.Cipher import ARC4, AES
|
||||
from Crypto.Hash import HMAC
|
||||
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
|
||||
|
||||
from Crypto.Cipher import ARC4, DES, AES
|
||||
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()
|
||||
|
||||
yield vma
|
||||
except exceptions.InvalidAddressException:
|
||||
if not vma.is_valid():
|
||||
vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}")
|
||||
continue
|
||||
|
||||
yield vma
|
||||
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user