Merge branch 'volatilityfoundation:develop' into hsarkey/windows-dlllist

This commit is contained in:
Hannah Sarkey
2024-06-26 12:10:11 -04:00
committed by GitHub
50 changed files with 1860 additions and 197 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check --diff --verbose"
+3 -3
View File
@@ -20,9 +20,9 @@ jobs:
matrix:
python-version: ["3.7"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
@@ -37,7 +37,7 @@ jobs:
python setup.py bdist_wheel
- name: Archive dist
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: volatility3-pypi
path: |
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
host: [ ubuntu-latest, windows-latest ]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
+2 -2
View File
@@ -8,9 +8,9 @@ jobs:
matrix:
python-version: ["3.7"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+1 -1
View File
@@ -14,7 +14,7 @@ authors:
identifiers:
- type: url
value: 'https://github.com/volatilityfoundation/volatility3'
description: Volatility 3 source code respository
description: Volatility 3 source code repository
repository-code: 'https://github.com/volatilityfoundation/volatility3'
url: 'https://github.com/volatilityfoundation/volatility3'
abstract: >-
+4 -17
View File
@@ -1,22 +1,9 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
# This is required for the yara plugins
yara-python>=3.8.0
# This is required for several plugins that perform malware analysis and disassemble code.
# It can also improve accuracy of Windows 8 and later memory samples.
capstone>=3.0.5
# This is required by plugins that decrypt passwords, password hashes, etc.
pycryptodome
-r requirements.txt
# This can improve error messages regarding improperly configured ISF files,
# but is only recommended for development
jsonschema>=2.3.0
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
# Used to build executable file
pyinstaller>=6.5.0
pyinstaller-hooks-contrib>=2024.3
+2 -2
View File
@@ -1,5 +1,5 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# Include the minimal requirements
-r requirements-minimal.txt
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
+37
View File
@@ -6,6 +6,7 @@
#
import os
import re
import subprocess
import sys
import shutil
@@ -189,6 +190,16 @@ def test_windows_svcscan(image, volatility, python):
assert rc == 0
def test_windows_thrdscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.thrdscan.ThrdScan", image, volatility, python)
# find pid 4 (of system process) which starts with lowest tids
assert out.find(b"\t4\t8") != -1
assert out.find(b"\t4\t12") != -1
assert out.find(b"\t4\t16") != -1
#assert out.find(b"this raieses AssertionError") != -1
assert rc == 0
def test_windows_privileges(image, volatility, python):
rc, out, err = runvol_plugin(
"windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"]
@@ -331,6 +342,32 @@ def test_linux_tty_check(image, volatility, python):
assert rc == 0
def test_linux_library_list(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.library_list.LibraryList", image, volatility, python
)
assert re.search(
rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert re.search(
rb"gnome-settings-\s3807\s0x7f7e660b5000\s/lib/x86_64-linux-gnu/libbz2.so.1.0",
out,
)
assert re.search(
rb"gdu-notificatio\s3878\s0x7f25ce33e000\s/usr/lib/x86_64-linux-gnu/libXau.so.6",
out,
)
assert re.search(
rb"bash\s8600\s0x7fe78a85f000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert out.count(b"\n") >= 2677
assert rc == 0
# MAC
+34 -11
View File
@@ -264,12 +264,14 @@ class CommandLine:
file_logger.setFormatter(file_formatter)
rootlog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
if partial_args.verbosity < 1:
sys.tracebacklimit = None
console.setLevel(30 - (partial_args.verbosity * 10))
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
@@ -695,6 +697,17 @@ class CommandLine:
)
context.config[extended_path] = value
def order_extra_verbose_levels(self):
for level, level_value in enumerate(
[
constants.LOGLEVEL_V,
constants.LOGLEVEL_VV,
constants.LOGLEVEL_VVV,
constants.LOGLEVEL_VVVV,
]
):
logging.addLevelName(level_value, f"DETAIL {level+1}")
def file_handler_class_factory(self, direct=True):
output_dir = self.output_dir
@@ -703,19 +716,17 @@ class CommandLine:
"""Gets the final filename"""
if output_dir is None:
raise TypeError("Output directory is not a string")
os.makedirs(output_dir, exist_ok=True)
pref_name_array = self.preferred_filename.split(".")
filename, extension = (
os.path.join(output_dir, ".".join(pref_name_array[:-1])),
pref_name_array[-1],
)
output_filename = f"{filename}.{extension}"
output_filename = os.path.join(output_dir, self.preferred_filename)
filename, extension = os.path.splitext(output_filename)
counter = 1
while os.path.exists(output_filename):
output_filename = f"{filename}-{counter}.{extension}"
output_filename = f"{filename}-{counter}{extension}"
counter += 1
return output_filename
class CLIMemFileHandler(io.BytesIO, CLIFileHandler):
@@ -778,8 +789,16 @@ class CommandLine:
if self._file.closed:
return None
self._file.close()
output_filename = self._get_final_filename()
# Update the filename, which may have changed if a file with
# the same name already existed. This needs to be done before
# closing the file, otherwise FileHandlerInterface will raise
# an exception. Also, the preferred_filename setter only allows
# a specific set of characters, where '/' is not in that list
self.preferred_filename = os.path.basename(output_filename)
self._file.close()
os.rename(self._name, output_filename)
if direct:
@@ -827,7 +846,11 @@ class CommandLine:
requirement,
volatility3.framework.configuration.requirements.ListRequirement,
):
additional["type"] = requirement.element_type
# Allow a list of integers, specified with the convenient 0x hexadecimal format
if requirement.element_type == int:
additional["type"] = lambda x: int(x, 0)
else:
additional["type"] = requirement.element_type
nargs = "*" if requirement.optional else "+"
additional["nargs"] = nargs
elif isinstance(
+3 -2
View File
@@ -197,10 +197,11 @@ class VolShell(cli.CommandLine):
vollog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
console.setLevel(30 - (partial_args.verbosity * 10))
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
+7 -1
View File
@@ -223,8 +223,14 @@ def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
return plugin_list
def clear_cache(complete=False):
def clear_cache(complete=True):
try:
if complete:
glob_pattern = "*.cache"
for cache_filename in glob.glob(
os.path.join(constants.CACHE_PATH, glob_pattern)
):
os.unlink(cache_filename)
os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME))
except FileNotFoundError:
vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache")
+4 -1
View File
@@ -159,7 +159,10 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
# This we get for free
aslr_shift = (
init_task.files.cast("long unsigned int")
int.from_bytes(
init_task.files.cast("bytes", length=init_task.files.vol.size),
byteorder=init_task.files.vol.data_format.byteorder,
)
- module.get_symbol("init_files").address
)
kaslr_shift = init_task_address - cls.virtual_to_physical_address(
+9 -5
View File
@@ -44,7 +44,7 @@ BANG = "!"
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 6 # Number of changes that only add to the interface
VERSION_MINOR = 7 # Number of changes that only add to the interface
VERSION_PATCH = 1 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -59,14 +59,18 @@ PACKAGE_VERSION = (
AUTOMAGIC_CONFIG_PATH = "automagic"
"""The root section within the context configuration for automagic values"""
LOGLEVEL_INFO = 20
"""Logging level for information data, showed when use the requests any logging: -v"""
LOGLEVEL_DEBUG = 10
"""Logging level for debugging data, showed when the user requests more logging detail: -vv"""
LOGLEVEL_V = 9
"""Logging level for a single -v"""
"""Logging level for the lowest "extra" level of logging: -vvv"""
LOGLEVEL_VV = 8
"""Logging level for -vv"""
"""Logging level for two levels of detail: -vvvv"""
LOGLEVEL_VVV = 7
"""Logging level for -vvv"""
"""Logging level for three levels of detail: -vvvvv"""
LOGLEVEL_VVVV = 6
"""Logging level for -vvvv"""
"""Logging level for four levels of detail: -vvvvvv"""
CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3")
"""Default path to store cached data"""
@@ -5,11 +5,10 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import IntEnum
KERNEL_NAME = "__kernel__"
# arch/x86/include/asm/page_types.h
PAGE_SHIFT = 12
"""The value hard coded from the Linux Kernel (hence not extracted from the layer itself)"""
# include/linux/sched.h
@@ -281,3 +280,25 @@ CAPABILITIES = (
)
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1
class ELF_IDENT(IntEnum):
"""ELF header e_ident indexes"""
EI_MAG0 = 0
EI_MAG1 = 1
EI_MAG2 = 2
EI_MAG3 = 3
EI_CLASS = 4
EI_DATA = 5
EI_VERSION = 6
EI_OSABI = 7
EI_PAD = 8
class ELF_CLASS(IntEnum):
"""ELF header class types"""
ELFCLASSNONE = 0
ELFCLASS32 = 1
ELFCLASS64 = 2
+5 -4
View File
@@ -256,12 +256,13 @@ class Module(interfaces.context.ModuleInterface):
if not absolute:
offset += self._offset
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if "layer_name" in kwargs:
del kwargs["layer_name"]
# We have to allow using an alternative layer name due to pool scanners switching
# to the memory layer for scanning samples prior to Windows 10.
layer_name = kwargs.pop("layer_name", self._layer_name)
return self._context.object(
object_type=object_type,
layer_name=self._layer_name,
layer_name=layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
**kwargs,
+1 -1
View File
@@ -60,7 +60,7 @@ class FileHandlerInterface(io.RawIOBase):
@staticmethod
def sanitize_filename(filename: str) -> str:
"""Sanititizes the filename to ensure only a specific whitelist of characters is allowed through"""
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]\{\}!$%^:#~?<>,|"
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]{}!$%^:#~?<>,|"
result = ""
for char in filename:
if char in allowed:
+66 -44
View File
@@ -96,12 +96,13 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
def get_summary_header(self) -> interfaces.objects.ObjectInterface:
return self.context.object(
self._crash_common_table_name + constants.BANG + "_SUMMARY_DUMP",
offset=0x1000 * self.headerpages,
offset=self._page_size * self.headerpages,
layer_name=self._base_layer,
)
def _load_segments(self) -> None:
"""Loads up the segments from the meta_layer."""
"""Loads up the segments from the meta_layer.
A segment is a set of contiguous memory pages."""
segments = []
@@ -119,70 +120,87 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
for run in header.PhysicalMemoryBlockBuffer.Run:
segments.append(
(
run.BasePage * 0x1000,
offset * 0x1000,
run.PageCount * 0x1000,
run.PageCount * 0x1000,
run.BasePage * self._page_size,
offset * self._page_size,
run.PageCount * self._page_size,
run.PageCount * self._page_size,
)
)
offset += run.PageCount
elif self.dump_type == 0x05:
summary_header = self.get_summary_header()
first_bit = None # First bit in a run
first_offset = 0 # File offset of first bit
last_bit_seen = 0 # Most recent bit processed
offset = summary_header.HeaderSize # Size of file headers
buffer_char = summary_header.get_buffer_char()
buffer_long = summary_header.get_buffer_long()
for outer_index in range(0, ((summary_header.BitmapSize + 31) // 32)):
if buffer_long[outer_index] == 0:
if first_bit is not None:
last_bit = ((outer_index - 1) * 32) + 31
segment_length = (last_bit - first_bit + 1) * 0x1000
seg_first_bit = None # First bit in a run
seg_first_offset = 0 # File offset of first bit
offset = (
summary_header.HeaderSize
) # Offset to the start of actual memory dump
ulong_bitmap_array = summary_header.get_buffer_long()
# outer_index points to a 32 bits array inside a list of arrays,
# each bit indicating a page mapping state
for outer_index in range(0, ulong_bitmap_array.vol.count):
ulong_bitmap = ulong_bitmap_array[outer_index]
# All pages in this 32 bits array are mapped (speedup iteration process)
if ulong_bitmap == 0xFFFFFFFF:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = outer_index * 32
offset += 32 * self._page_size
# No pages in this 32 bits array are mapped (speedup iteration process)
elif ulong_bitmap == 0:
# End of segment
if seg_first_bit is not None:
last_bit = (outer_index - 1) * 32 + 31
segment_length = (
last_bit - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
elif buffer_long[outer_index] == 0xFFFFFFFF:
if first_bit is None:
first_offset = offset
first_bit = outer_index * 32
offset = offset + (32 * 0x1000)
seg_first_bit = None
# Some pages in this 32 bits array are mapped and some aren't
else:
for inner_index in range(0, 32):
bit_addr = outer_index * 32 + inner_index
if (buffer_char[bit_addr >> 3] >> (bit_addr & 0x7)) & 1:
if first_bit is None:
first_offset = offset
first_bit = bit_addr
offset = offset + 0x1000
for inner_bit_position in range(0, 32):
current_bit = outer_index * 32 + inner_bit_position
page_mapped = ulong_bitmap & (1 << inner_bit_position)
if page_mapped:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = current_bit
offset += self._page_size
else:
if first_bit is not None:
# End of segment
if seg_first_bit is not None:
segment_length = (
(bit_addr - 1) - first_bit + 1
) * 0x1000
current_bit - 1 - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
last_bit_seen = (outer_index * 32) + 31
seg_first_bit = None
last_bit_seen = outer_index * 32 + 31
if first_bit is not None:
segment_length = (last_bit_seen - first_bit + 1) * 0x1000
if seg_first_bit is not None:
segment_length = (last_bit_seen - seg_first_bit + 1) * self._page_size
segments.append(
(first_bit * 0x1000, first_offset, segment_length, segment_length)
(
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
else:
vollog.log(
@@ -261,11 +279,15 @@ class WindowsCrashDumpStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
for layer in [WindowsCrashDump32Layer, WindowsCrashDump64Layer]:
with contextlib.suppress(WindowsCrashDumpFormatException):
try:
layer.check_header(context.layers[layer_name])
new_name = context.layers.free_layer_name(layer.__name__)
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return layer(context, new_name, new_name)
except WindowsCrashDumpFormatException as excp:
vollog.log(
constants.LOGLEVEL_VVVV, f"Exception reading crashdump: {excp}"
)
return None
+13 -2
View File
@@ -6,9 +6,11 @@ import struct
from typing import Optional
from volatility3.framework import exceptions, interfaces, constants
from volatility3.framework.constants.linux import ELF_CLASS
from volatility3.framework.layers import segmented
from volatility3.framework.symbols import intermed
vollog = logging.getLogger(__name__)
@@ -21,7 +23,7 @@ class Elf64Layer(segmented.SegmentedLayer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -50,8 +52,17 @@ class Elf64Layer(segmented.SegmentedLayer):
offset=ehdr.e_phoff + (pindex * ehdr.e_phentsize),
)
# We only want PT_TYPES with valid sizes
try:
ptype = phdr.p_type.description
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
if (
phdr.p_type.lookup() == "PT_LOAD"
ptype == "PT_LOAD"
and phdr.p_filesz == phdr.p_memsz
and phdr.p_filesz > 0
):
+12
View File
@@ -67,6 +67,12 @@ class Intel(linear.LinearlyMappedLayer):
math.ceil(math.log2(struct.calcsize(self._entry_format)))
)
@classproperty
@functools.lru_cache()
def page_shift(cls) -> int:
"""Page shift for the intel memory layers."""
return cls._page_size_in_bits
@classproperty
@functools.lru_cache()
def page_size(cls) -> int:
@@ -76,6 +82,12 @@ class Intel(linear.LinearlyMappedLayer):
"""
return 1 << cls._page_size_in_bits
@classproperty
@functools.lru_cache()
def page_mask(cls) -> int:
"""Page mask for the intel memory layers."""
return ~(cls.page_size - 1)
@classproperty
@functools.lru_cache()
def bits_per_register(cls) -> int:
@@ -151,6 +151,12 @@ class ResourceAccessor(object):
raise excp
else:
raise excp
except ValueError as excp:
# Reraise errors such as proxy auth errors as offline exception errors
# Example Proxy auth error - ValueError: AbstractDigestAuthHandler does not support the following scheme: 'Negotiate'
vollog.info(f"Cannot access {url} due to {excp} - Setting OFFLINE")
constants.OFFLINE = True
raise exceptions.OfflineException(url)
except exceptions.OfflineException:
vollog.info(f"Not accessing {url} in offline mode")
raise
+1 -1
View File
@@ -152,7 +152,7 @@ class NonLinearlySegmentedLayer(
raise ValueError("SegmentedLayer must contain some segments")
if self._maxaddr is None:
mapped, _, length, _ = self._segments[-1]
self._maxaddr = mapped + length
self._maxaddr = mapped + length - 1
return self._maxaddr
@property
+4 -5
View File
@@ -5,6 +5,7 @@ from typing import Optional
from volatility3.framework import constants, interfaces, exceptions
from volatility3.framework.layers import elf
from volatility3.framework.symbols import intermed
from volatility3.framework.constants.linux import ELF_CLASS
vollog = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -115,12 +116,10 @@ class XenCoreDumpLayer(elf.Elf64Layer):
)
)
elif p2m_data and pfn_data:
raise elf.ElfFormatException(
self.name, f"Both P2M and PFN in Xen Core Dump"
)
raise elf.ElfFormatException(self.name, "Both P2M and PFN in Xen Core Dump")
else:
raise elf.ElfFormatException(
self.name, f"Neither P2M nor PFN in Xen Core Dump"
self.name, "Neither P2M nor PFN in Xen Core Dump"
)
if len(segments) == 0:
+10 -7
View File
@@ -18,7 +18,7 @@ class LayerWriter(plugins.PluginInterface):
default_block_size = 0x500000
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -77,7 +77,7 @@ class LayerWriter(plugins.PluginInterface):
file_handle = open_method(preferred_name)
for i in range(0, layer.maximum_address, chunk_size):
current_chunk_size = min(chunk_size, layer.maximum_address - i)
current_chunk_size = min(chunk_size, layer.maximum_address + 1 - i)
data = layer.read(i, current_chunk_size, pad=True)
file_handle.write(data)
if progress_callback:
@@ -95,7 +95,7 @@ class LayerWriter(plugins.PluginInterface):
if not self.config["layers"]:
self.config["layers"] = []
for name in self.context.layers:
if not self.context.layers[name].metadata.get("mapped", False):
if "mapped" not in self.context.layers[name].metadata:
self.config["layers"] = [name]
for name in self.config["layers"]:
@@ -103,7 +103,8 @@ class LayerWriter(plugins.PluginInterface):
if name not in self.context.layers:
yield 0, (f"Layer Name {name} does not exist",)
else:
output_name = self.config.get("output", ".".join([name, "raw"]))
default_output_name = f"{name}.raw"
output_name = self.config.get("output", default_output_name)
try:
file_handle = self.write_layer(
self.context,
@@ -114,10 +115,12 @@ class LayerWriter(plugins.PluginInterface):
progress_callback=self._progress_callback,
)
file_handle.close()
# Update the filename, which may have changed if a file
# with the same name already existed.
output_name = file_handle.preferred_filename
except IOError as excp:
yield 0, (
f"Layer cannot be written to {self.config['output_name']}: {excp}",
)
yield 0, (f"Layer cannot be written to {output_name}: {excp}",)
yield 0, (f"Layer has been written to {output_name}",)
+20 -15
View File
@@ -14,8 +14,10 @@ from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.framework.constants.linux import ELF_MAX_EXTRACTION_SIZE
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
@@ -23,7 +25,7 @@ class Elfs(plugins.PluginInterface):
"""Lists all memory mapped ELF files for all processes."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -82,12 +84,20 @@ class Elfs(plugins.PluginInterface):
)
if not elf_object.is_valid():
vollog.debug("ELF object to be dumped is not valid")
return None
sections = {}
# TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ?
for phdr in elf_object.get_program_headers():
if phdr.p_type != 1: # PT_LOAD = 1
try:
if phdr.p_type.description != "PT_LOAD":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
start = phdr.p_vaddr
@@ -95,18 +105,18 @@ class Elfs(plugins.PluginInterface):
end = start + size
# Use complete memory pages for dumping
# If start isn't a multiple of 4096, stick to the highest multiple < start
# If end isn't a multiple of 4096, stick to the lowest multiple > end
if start % 4096:
start = start & ~0xFFF
# If start isn't a multiple of a page, stick to the highest multiple < start
# If end isn't a multiple of a page, stick to the lowest multiple > end
if start % proc_layer.page_size:
start = start & proc_layer.page_mask
if end % 4096:
end = (end & ~0xFFF) + 4096
if end % proc_layer.page_size:
end = (end & proc_layer.page_mask) + proc_layer.page_size
real_size = end - start
# Check if ELF has a legitimate size
if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE:
if real_size < 0 or real_size > ELF_MAX_EXTRACTION_SIZE:
raise ValueError(f"The claimed size of the ELF is invalid: {real_size}")
sections[start] = real_size
@@ -140,12 +150,7 @@ class Elfs(plugins.PluginInterface):
for vma in task.mm.get_vma_iter():
hdr = proc_layer.read(vma.vm_start, 4, pad=True)
if not (
hdr[0] == 0x7F
and hdr[1] == 0x45
and hdr[2] == 0x4C
and hdr[3] == 0x46
):
if hdr != b"\x7fELF":
continue
path = vma.get_name(self.context, task)
+4 -2
View File
@@ -66,7 +66,7 @@ class ABCKmsg(ABC):
self._config = config
self.vmlinux = context.modules[self._config["kernel"]]
self.layer_name = self.vmlinux.layer_name # type: ignore
self.long_unsigned_int_size = self.vmlinux.get_type("long unsigned int").size
self.long_unsigned_int_size = self.vmlinux.get_type("pointer").size
@classmethod
def run_all(
@@ -198,7 +198,9 @@ class ABCKmsg(ABC):
class Kmsg_pre_3_5(ABCKmsg):
"""The kernel ring buffer (log_buf) is a char array that sequentially stores
log lines, each separated by newline (LF) characters. i.e:
<6>[ 9565.250411] line1!\n<6>[ 9565.250412] line2\n...
<6>[ 9565.250411] line1!\\n<6>[ 9565.250412] line2\\n...
"""
@classmethod
@@ -0,0 +1,169 @@
# 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
from typing import Iterable, Tuple
from volatility3.framework import interfaces, renderers, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class LibraryList(interfaces.plugins.PluginInterface):
"""Enumerate libraries loaded into processes"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 2, 0)
),
requirements.ListRequirement(
name="pids",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _get_libdl_libraries(
self, proc_layer_name: str, vma_start: int
) -> interfaces.objects.ObjectInterface:
"""Get the ELF link map objects for the given VMA address
Args:
proc_layer_name (str): Name of the process layer
vma_start (int): VMA start address
Yields:
ELF link map objects for the given VMA address
"""
elf_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"linux",
"elf",
class_types=elf.class_types,
)
elf_object = self.context.object(
elf_table_name + constants.BANG + "Elf",
offset=vma_start,
layer_name=proc_layer_name,
)
if not elf_object or not elf_object.is_valid():
return None
kernel = self.context.modules[self.config["kernel"]]
try:
for link_map in elf_object.get_link_maps(kernel.symbol_table_name):
if link_map.l_addr and link_map.l_name:
yield link_map
except exceptions.InvalidAddressException:
# Protection against memory smear in this VMA
pass
def _get_libdl_maps(
self, task: interfaces.objects.ObjectInterface, proc_layer_name: str
) -> interfaces.objects.ObjectInterface:
"""Get the ELF link maps objects for a task
Args:
task (task_struct): A reference task
proc_layer_name (str): Name of the process layer
Yields:
ELF link map objects
"""
link_map_seen = set()
for vma in task.mm.get_vma_iter():
for link_map in self._get_libdl_libraries(proc_layer_name, vma.vm_start):
if link_map.l_addr in link_map_seen:
continue
yield link_map
link_map_seen.add(link_map.l_addr)
def _get_task_libraries(
self, task: interfaces.objects.ObjectInterface
) -> Tuple[int, str]:
"""Get the task libraries from the ELF headers found within the memory maps
Args:
task (task_struct): The reference task
Yields:
Tuples with a ELF link map address and name
"""
proc_layer_name = task.add_process_layer()
if not proc_layer_name:
return
for elf_link_map in self._get_libdl_maps(task, proc_layer_name):
name = elf_link_map.get_name()
if not name:
continue
yield elf_link_map.l_addr, name
def _get_tasks_libraries(
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
) -> Iterable[Tuple[str, int, int, str]]:
"""Get the task libraries from the ELF headers found within the memory maps for
all the tasks.
Args:
tasks: An iterable of tasks
Yields:
Tuples with a task name, task tgid, an ELF link map address and name
"""
for task in tasks:
task_name = utility.array_to_string(task.comm)
for linkmap_addr, linkmap_name in self._get_task_libraries(task):
yield task_name, task.tgid, linkmap_addr, linkmap_name
def _format_fields(self, fields):
task_name, task_pid, addr, name = fields
return task_name, task_pid, format_hints.Hex(addr), name
def _generator(
self, tasks: Iterable[interfaces.objects.ObjectInterface]
) -> Iterable[Tuple[int, Tuple]]:
for fields in self._get_tasks_libraries(tasks):
yield 0, self._format_fields(fields)
def run(self):
pids = self.config.get("pids")
pid_filter = pslist.PsList.create_pid_filter(pids)
tasks = pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=pid_filter
)
headers = [
("Name", str),
("Pid", int),
("LoadAddress", format_hints.Hex),
("Path", str),
]
return renderers.TreeGrid(headers, self._generator(tasks))
@@ -83,11 +83,11 @@ class PsList(interfaces.plugins.PluginInterface):
cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, str]:
"""Extract the fields needed for the final output
Args:
task: A task object from where to get the fields.
decorate_comm: If True, it decorates the comm string of
- User threads: in curly brackets,
- Kernel threads: in square brackets
decorate_comm: If True, it decorates the comm string of user threads in curly brackets,
and of Kernel threads in square brackets.
Defaults to False.
Returns:
A tuple with the fields to show in the plugin output.
@@ -142,6 +142,8 @@ class PsList(interfaces.plugins.PluginInterface):
file_output = str(file_handle.preferred_filename)
file_handle.close()
break
else:
file_output = "VMA start matching task start_code not found"
return file_output
def _generator(
@@ -83,7 +83,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
sock: Kernel generic `sock` object
Returns a tuple with:
sock: The respective kernel's \*_sock object for that socket family
sock: The respective kernel's \\*_sock object for that socket family
sock_stat: A tuple with the source and destination (address and port) along with its state string
socket_filter: A dictionary with information about the socket filter
"""
@@ -501,7 +501,7 @@ class Sockstat(plugins.PluginInterface):
family: Socket family string (AF_UNIX, AF_INET, etc)
sock_type: Socket type string (STREAM, DGRAM, etc)
protocol: Protocol string (UDP, TCP, etc)
sock_fields: A tuple with the \*_sock object, the sock stats and the extended info dictionary
sock_fields: A tuple with the \\*_sock object, the sock stats and the extended info dictionary
"""
vmlinux = context.modules[symbol_table]
@@ -0,0 +1,79 @@
# 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
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
vollog = logging.getLogger(__name__)
class Dmesg(interfaces.plugins.PluginInterface):
"""Prints the kernel log buffer."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def get_kernel_log_buffer(
cls, context: interfaces.context.ContextInterface, kernel_module_name: str
):
"""
Online documentation :
- https://github.com/apple-open-source/macos/blob/master/xnu/bsd/sys/msgbuf.h
- https://github.com/apple-open-source/macos/blob/ea4cd5a06831aca49e33df829d2976d6de5316ec/xnu/bsd/kern/subr_log.c#L751
Volatility 2 plugin :
- https://github.com/volatilityfoundation/volatility/blob/master/volatility/plugins/mac/dmesg.py
"""
kernel = context.modules[kernel_module_name]
if not kernel.has_symbol("msgbufp"):
raise exceptions.SymbolError(
"msgbufp",
kernel.symbol_table_name,
'The provided symbol table does not include the "msgbufp" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.',
)
msgbufp = kernel.object_from_symbol(symbol_name="msgbufp")
msg_size = msgbufp.msg_size # max buffer size
msg_bufx = msgbufp.msg_bufx # write index of the msg_bufc circular buffer
msg_bufc = msgbufp.msg_bufc
# msg_bufc is circular, meaning that if its size exceeds msg_size,
# msg_bufx will point to the beginning of the buffer and start overwriting.
msg_bufc_data: str = utility.pointer_to_string(msg_bufc, msg_size)
# Avoid OOB reads
msg_bufx = msg_bufx if msg_bufx <= msg_size else 0
# We directly take into account the case where the write buffer did a loop,
# as older messages will start at msg_bufx offset (not overwritten yet).
dmesg = msg_bufc_data[msg_bufx:]
dmesg += msg_bufc_data[:msg_bufx]
# Yield each line
for dmesg_line in dmesg.splitlines():
yield (dmesg_line,)
def _generator(self):
for value in self.get_kernel_log_buffer(
context=self.context, kernel_module_name=self.config["kernel"]
):
yield (0, value)
def run(self):
return renderers.TreeGrid(
[
("line", str),
],
self._generator(),
)
+166 -4
View File
@@ -2,17 +2,23 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from volatility3.framework import renderers, interfaces
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.mac import pslist
from typing import Callable, Generator, Type, Optional
import logging
vollog = logging.getLogger(__name__)
class Maps(interfaces.plugins.PluginInterface):
"""Lists process memory ranges that potentially contain injected code."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
@classmethod
def get_requirements(cls):
@@ -31,14 +37,152 @@ class Maps(interfaces.plugins.PluginInterface):
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed memory segments",
default=False,
optional=True,
),
requirements.ListRequirement(
name="address",
description="Process virtual memory addresses to include "
"(all other VMA sections are excluded). This can be any "
"virtual address within the VMA section. Virtual addresses "
"must be separated by a space.",
element_type=int,
optional=True,
),
requirements.IntRequirement(
name="maxsize",
description="Maximum size for dumped VMA sections "
"(all the bigger sections will be ignored)",
default=cls.MAXSIZE_DEFAULT,
optional=True,
),
]
@classmethod
def list_vmas(
cls,
task: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: True,
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Memory Areas of a specific process.
Args:
task: task object from which to list the vma
filter_func: Function to take a vma and return False if it should be filtered out
Returns:
Yields vmas based on the task and filtered based on the filter function
"""
for vma in task.get_map_iter():
if filter_func(vma):
yield vma
else:
vollog.debug(
f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.p_pid} due to filter_func"
)
@classmethod
def vma_dump(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
vm_start: int,
vm_end: int,
open_method: Type[interfaces.plugins.FileHandlerInterface],
maxsize: int = MAXSIZE_DEFAULT,
) -> Optional[interfaces.plugins.FileHandlerInterface]:
"""Extracts the complete data for VMA as a FileInterface.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
task: an task_struct instance
vm_start: The start virtual address from the vma to dump
vm_end: The end virtual address from the vma to dump
open_method: class to provide context manager for opening the file
maxsize: Max size of VMA section (default MAXSIZE_DEFAULT)
Returns:
An open FileInterface object containing the complete data for the task or None in the case of failure
"""
pid = task.p_pid
try:
proc_layer_name = task.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
pid, excp.invalid_address, excp.layer_name
)
)
return None
vm_size = vm_end - vm_start
# check if vm_size is negative, this should never happen.
if vm_size < 0:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative."
)
return None
# check if vm_size is larger than the maxsize limit, and therefore is not saved out.
if maxsize <= vm_size:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}"
)
return None
proc_layer = context.layers[proc_layer_name]
file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp"
try:
file_handle = open_method(file_name)
chunk_size = 1024 * 1024 * 10
offset = vm_start
while offset < vm_start + vm_size:
to_read = min(chunk_size, vm_start + vm_size - offset)
data = proc_layer.read(offset, to_read, pad=True)
file_handle.write(data)
offset += to_read
except Exception as excp:
vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}")
return None
return file_handle
def _generator(self, tasks):
address_list = self.config.get("address", None)
if not address_list:
# do not filter as no address_list was supplied
vma_filter_func = lambda _: True
else:
# filter for any vm_start that matches the supplied address config
def vma_filter_function(task: interfaces.objects.ObjectInterface) -> bool:
addrs_in_vma = [
addr
for addr in address_list
if task.links.start <= addr <= task.links.end
]
# if any of the user supplied addresses would fall within this vma return true
return bool(addrs_in_vma)
vma_filter_func = vma_filter_function
for task in tasks:
process_name = utility.array_to_string(task.p_comm)
process_pid = task.p_pid
for vma in task.get_map_iter():
for vma in self.list_vmas(task, filter_func=vma_filter_func):
try:
vm_start = vma.links.start
vm_end = vma.links.end
except AttributeError:
vollog.debug(
f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {process_pid}"
)
continue
path = vma.get_path(
self.context,
self.context.modules[self.config["kernel"]].symbol_table_name,
@@ -46,15 +190,32 @@ class Maps(interfaces.plugins.PluginInterface):
if path == "":
path = vma.get_special_path()
file_output = "Disabled"
if self.config["dump"]:
file_output = "Error outputting file"
file_handle = self.vma_dump(
self.context,
task,
vm_start,
vm_end,
self.open,
self.config["maxsize"],
)
if file_handle:
file_handle.close()
file_output = file_handle.preferred_filename
yield (
0,
(
process_pid,
process_name,
format_hints.Hex(vma.links.start),
format_hints.Hex(vma.links.end),
format_hints.Hex(vm_start),
format_hints.Hex(vm_end),
vma.get_perms(),
path,
file_output,
),
)
@@ -72,6 +233,7 @@ class Maps(interfaces.plugins.PluginInterface):
("End", format_hints.Hex),
("Protection", str),
("Map Name", str),
("File output", str),
],
self._generator(
list_tasks(self.context, self.config["kernel"], filter_func=filter_func)
+4 -4
View File
@@ -183,16 +183,16 @@ class Timeliner(interfaces.plugins.PluginInterface):
plugin_name,
self._sanitize_body_format(item),
self._text_format(
times.get(TimeLinerType.ACCESSED, "")
times.get(TimeLinerType.ACCESSED, "0")
),
self._text_format(
times.get(TimeLinerType.MODIFIED, "")
times.get(TimeLinerType.MODIFIED, "0")
),
self._text_format(
times.get(TimeLinerType.CHANGED, "")
times.get(TimeLinerType.CHANGED, "0")
),
self._text_format(
times.get(TimeLinerType.CREATED, "")
times.get(TimeLinerType.CREATED, "0")
),
)
)
+217
View File
@@ -0,0 +1,217 @@
# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import enum
import logging
import os
import struct
from typing import Dict, List
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import configuration, plugins
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
vollog = logging.getLogger(__name__)
class VMCSTest(enum.IntFlag):
VMCS_ABORT_INVALID = enum.auto()
VMCS_LINK_PTR_IS_NOT_FS = enum.auto()
VMCS_HOST_CR4_NO_VTX = enum.auto()
VMCS_CR3_IS_ZERO = enum.auto()
VMCS_GUEST_CR4_RESERVED = enum.auto()
class PageStartScanner(interfaces.layers.ScannerInterface):
def __init__(self, signatures: List[bytes], page_size: int = 0x1000):
super().__init__()
if not len(signatures):
raise ValueError("No signatures passed to constructor")
self._siglen = len(signatures[0])
for item in signatures:
if len(item) != self._siglen:
raise ValueError(
"Signatures of different lengths passed to PageStartScanner"
)
self._signatures = signatures
self._page_size = page_size
def __call__(self, data: bytes, data_offset: int):
"""Scans only the start of every page, to see whether a signature is present or not"""
for page_start in range(
data_offset % self._page_size, len(data), self._page_size
):
if data[page_start : page_start + self._siglen] in self._signatures:
yield (
page_start + data_offset,
data[page_start : page_start + self._siglen],
)
class Vmscan(plugins.PluginInterface):
"""Scans for Intel VT-d structues and generates VM volatility configs for them"""
_required_framework_version = (2, 2, 0)
_version = (1, 0, 0)
STRICTLY_REQUIRED_TESTS = {
VMCSTest.VMCS_ABORT_INVALID,
VMCSTest.VMCS_LINK_PTR_IS_NOT_FS,
VMCSTest.VMCS_HOST_CR4_NO_VTX,
}
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.TranslationLayerRequirement(
name="primary", description="Physical base memory layer"
),
requirements.IntRequirement(
name="log-threshold",
description="Number of criteria failed to log to debug output",
default=2,
optional=True,
),
]
# Scan for VMCS structures based on the known VMCS structures
# found in symbols/vmcs directory
def _gather_vmcs_structures(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[bytes, str]:
"""Enumerate all JSON files containing VMCS information and return the structures
Signatures can be generated using data extracted using the vmcs_layout tool at
https://github.com/google/rekall/tree/master/tools/linux/vmcs_layout
Args:
context: The volatility context to work against
config_path: The location to store symbol table configurations under
Returns:
A dictionary of pattern bytes to the string representation of the architecture
"""
filenames = intermed.IntermediateSymbolTable.file_symbol_url(
os.path.join("generic", "vmcs")
)
table_names = []
for filename in filenames:
base_name = os.path.basename(filename).split(".")[0]
table_name = intermed.IntermediateSymbolTable.create(
context,
configuration.path_join(config_path, "vmcs"),
os.path.join("generic", "vmcs"),
filename=base_name,
)
table_names.append(table_name)
result = {}
for table_name in table_names:
symbol_table = context.symbol_space[table_name]
revision_id = struct.pack(
"<I", int(symbol_table.get_symbol("revision_id").constant_data)
)
result[revision_id] = table_name
return result
@classmethod
def _verify_vmcs_page(
cls,
context: interfaces.context.ContextInterface,
vmcs: interfaces.objects.ObjectInterface,
) -> List[str]:
"""Runs tests to verify whether a block of data is a VMCS page
Some tests based on the Hypervisor Memory Forensics paper by
Mariano Graziano, Andrea Lanzi and Davide Balzarotti
Args:
context: The volatility context to be used for this call
vmcs: The instantiated VMCS object to verify
Returns:
The list of failed criteria that the VMCS did not meet
"""
# The VMCS should have been constructed on the physical layer (even a nested VMCS)
physical_layer_name = vmcs.vol.layer_name
failed_tests: VMCSTest = VMCSTest(0)
# The abort field must be valid (generally 0, although other abort codes may exist)
if context.layers[physical_layer_name].read(vmcs.vol.offset + 4, 4) not in [
b"\x00\x00\x00\x00"
]:
failed_tests |= VMCSTest.VMCS_ABORT_INVALID
# The vmcs link pointer is supposed to always be set
if vmcs.vmcs_link_ptr != 0xFFFFFFFFFFFFFFFF:
failed_tests |= VMCSTest.VMCS_LINK_PTR_IS_NOT_FS
# To have a VMCS the host needs the VTx bit set in CR4, this can false positive often when all bits are set
if (vmcs.host_cr4 & 1 << 13) == 0:
failed_tests |= VMCSTest.VMCS_HOST_CR4_NO_VTX
# The guest CR3 is *exceptionally* unlikely to be 0 and the guest cr4 is likely to have some bits unset
if (vmcs.guest_cr3 == 0) or (vmcs.host_cr3 == 0):
failed_tests |= VMCSTest.VMCS_CR3_IS_ZERO
# CR4 registers have certain bits reserved that should not be set
if vmcs.guest_cr4 & 0xFFFFFFFFFF889000:
failed_tests |= VMCSTest.VMCS_GUEST_CR4_RESERVED
if failed_tests and failed_tests.name:
failed_list = failed_tests.name.split("|")
return failed_list
return []
def _generator(self):
# Gather VMCS structures
structures = self._gather_vmcs_structures(self.context, self.config_path)
# Scan memory for them
layer = self.context.layers[self.config["primary"]]
# Try to move down to the highest physical layer
if layer.config.get("memory_layer"):
layer = self.context.layers[layer.config["memory_layer"]]
# Run the scan
for offset, match in layer.scan(
self.context,
PageStartScanner(list(structures.keys())),
self._progress_callback,
):
try:
vmcs = self.context.object(
structures[match] + constants.BANG + "_VMCS",
layer.name,
offset=offset,
)
failed_list = self._verify_vmcs_page(self.context, vmcs)
if not failed_list:
yield (
0,
(
structures[match],
format_hints.Hex(vmcs.vol.offset),
format_hints.Hex(vmcs.ept),
format_hints.Hex(vmcs.guest_cr3),
),
)
if len(failed_list) <= self.config["log-threshold"]:
vollog.debug(
f"Potential {structures[match]} VMCS found at {vmcs.vol.offset:x} with failed criteria: {failed_list}"
)
except (exceptions.InvalidAddressException, AttributeError):
# Not what we're looking for
continue
def run(self):
return renderers.TreeGrid(
[
("Architecture", str),
("VMCS Physical offset", format_hints.Hex),
("EPT", format_hints.Hex),
("Guest CR3", format_hints.Hex),
],
self._generator(),
)
@@ -81,7 +81,10 @@ class DriverIrp(interfaces.plugins.PluginInterface):
address
)
module_found = False
for module_name, symbol_generator in module_symbols:
module_found = True
symbols_found = False
for symbol in symbol_generator:
@@ -111,6 +114,19 @@ class DriverIrp(interfaces.plugins.PluginInterface):
),
)
if not module_found:
yield (
0,
(
format_hints.Hex(driver.vol.offset),
driver_name,
MAJOR_FUNCTIONS[i],
format_hints.Hex(address),
renderers.NotAvailableValue(),
renderers.NotAvailableValue(),
),
)
def run(self):
return renderers.TreeGrid(
[
@@ -244,6 +244,8 @@ class DumpFiles(interfaces.plugins.PluginInterface):
symbol_table=kernel.symbol_table_name,
)
dumped_files = set()
for proc in procs:
try:
object_table = proc.ObjectTable
@@ -267,6 +269,10 @@ class DumpFiles(interfaces.plugins.PluginInterface):
if not file_re.search(name):
continue
if file_obj.vol.offset in dumped_files:
continue
dumped_files.add(file_obj.vol.offset)
for result in self.process_file_object(
self.context, kernel.layer_name, self.open, file_obj
):
@@ -303,6 +309,10 @@ class DumpFiles(interfaces.plugins.PluginInterface):
if not file_re.search(name):
continue
if file_obj.vol.offset in dumped_files:
continue
dumped_files.add(file_obj.vol.offset)
for result in self.process_file_object(
self.context, kernel.layer_name, self.open, file_obj
):
@@ -25,7 +25,7 @@ class Handles(interfaces.plugins.PluginInterface):
"""Lists process open handles."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -145,6 +145,9 @@ class Handles(interfaces.plugins.PluginInterface):
if self._sar_value is None:
if not has_capstone:
vollog.debug(
"capstone module is missing, unable to create disassembly of ObpCaptureHandleInformationEx"
)
return None
kernel = self.context.modules[self.config["kernel"]]
@@ -159,25 +162,46 @@ class Handles(interfaces.plugins.PluginInterface):
try:
func_addr = ntkrnlmp.get_symbol("ObpCaptureHandleInformationEx").address
except exceptions.SymbolError:
vollog.debug("Unable to locate ObpCaptureHandleInformationEx symbol")
return None
data = self.context.layers.read(virtual_layer_name, kvo + func_addr, 0x200)
if data is None:
try:
func_addr_to_read = kvo + func_addr
num_bytes_to_read = 0x200
vollog.debug(
f"ObpCaptureHandleInformationEx symbol located at {hex(func_addr_to_read)}"
)
data = self.context.layers.read(
virtual_layer_name, func_addr_to_read, num_bytes_to_read
)
except exceptions.InvalidAddressException:
vollog.debug(
f"Failed to read {hex(num_bytes_to_read)} bytes at symbol {hex(func_addr_to_read)}"
)
return None
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
instruction_count = 0
for address, size, mnemonic, op_str in md.disasm_lite(
data, kvo + func_addr
):
# print("{} {} {} {}".format(address, size, mnemonic, op_str))
instruction_count += 1
if mnemonic.startswith("sar"):
# if we don't want to parse op strings, we can disasm the
# single sar instruction again, but we use disasm_lite for speed
self._sar_value = int(op_str.split(",")[1].strip(), 16)
vollog.debug(
f"SAR located at {hex(address)} with value of {hex(self._sar_value)}"
)
break
if self._sar_value is None:
vollog.debug(
f"Failed to to locate SAR value having parsed {instruction_count} instructions"
)
return self._sar_value
@classmethod
@@ -1,3 +1,9 @@
# 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
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
@@ -5,12 +11,14 @@ from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import pslist, vadinfo
vollog = logging.getLogger(__name__)
class LdrModules(interfaces.plugins.PluginInterface):
"""Lists the loaded modules in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls):
@@ -71,7 +79,11 @@ class LdrModules(interfaces.plugins.PluginInterface):
# Filter out VADs that do not start with a MZ header
if dos_header.e_magic != 0x5A4D:
continue
except exceptions.PagedInvalidAddressException:
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping vad at {hex(dos_header.vol.offset)} due to InvalidAddressException",
)
continue
mapped_files[vad.get_start()] = vad.get_file_name()
@@ -155,12 +155,12 @@ class Malfind(interfaces.plugins.PluginInterface):
for proc in procs:
# by default, "Notes" column will be set to N/A
notes = renderers.NotApplicableValue()
process_name = utility.array_to_string(proc.ImageFileName)
for vad, data in self.list_injections(
self.context, kernel.layer_name, kernel.symbol_table_name, proc
):
notes = renderers.NotApplicableValue()
# Check for unique headers and update "Notes" column if criteria is met
if data[0:2] in refined_criteria:
notes = refined_criteria[data[0:2]]
@@ -38,7 +38,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -197,7 +197,7 @@ class ADS(interfaces.plugins.PluginInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -264,9 +264,10 @@ class ADS(interfaces.plugins.PluginInterface):
disasm = interfaces.renderers.Disassembly(
content, 0, architecture.lower()
)
content = format_hints.HexBytes(content)
else:
content = renderers.NotAvailableValue
disasm = interfaces.renderers.BaseAbsentValue
content = renderers.NotAvailableValue()
disasm = interfaces.renderers.BaseAbsentValue()
yield 0, (
format_hints.Hex(attr_data.vol.offset),
@@ -275,7 +276,7 @@ class ADS(interfaces.plugins.PluginInterface):
attr.Attr_Header.AttrType.lookup(),
file_name,
ads_name,
format_hints.HexBytes(content),
content,
disasm,
)
else:
@@ -222,6 +222,24 @@ class PoolScanner(plugins.PluginInterface):
type_name=symbol_table + constants.BANG + "_EPROCESS",
object_type="Process",
size=(600, None),
skip_type_test=True,
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# threads on windows before windows8
PoolConstraint(
b"Thr\xe5", # -> “protected” allocation, MSB is set.
type_name=symbol_table + constants.BANG + "_ETHREAD",
object_type="Thread",
size=(600, None), # -> 0x0258 - size of struct in win5.1
skip_type_test=True,
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# threads on windows starting with windows8
PoolConstraint(
b"Thre",
type_name=symbol_table + constants.BANG + "_ETHREAD",
object_type="Thread",
size=(600, None), # -> 0x0258 - size of struct in win5.1
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# files on windows before windows 8
@@ -0,0 +1,98 @@
# 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
from typing import List
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import ssdt
from volatility3.plugins.windows.registry import hivelist
vollog = logging.getLogger(__name__)
class GetCellRoutine(interfaces.plugins.PluginInterface):
"""Reports registry hives with a hooked GetCellRoutine handler"""
_required_framework_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
]
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
collection = ssdt.SSDT.build_module_collection(
self.context, kernel.layer_name, kernel.symbol_table_name
)
# walk each hive and validate that the GetCellRoutine handler
# is inside of the kernel (ntoskrnl)
for hive_object in hivelist.HiveList.list_hives(
context=self.context,
base_config_path=self.config_path,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
):
hive = hive_object.hive
try:
cellroutine = hive.GetCellRoutine
except exceptions.InvalidAddressException:
continue
module_symbols = list(
collection.get_module_symbols_by_absolute_location(cellroutine)
)
if module_symbols:
for module_name, _ in module_symbols:
# GetCellRoutine handlers should only be in the kernel
if module_name not in constants.windows.KERNEL_MODULE_NAMES:
yield (
0,
(
format_hints.Hex(hive.vol.offset),
hive_object.get_name() or "",
module_name,
format_hints.Hex(cellroutine),
),
)
# Doesn't map to any module...
else:
yield (
0,
(
format_hints.Hex(hive.vol.offset),
hive_object.get_name() or "",
renderers.NotAvailableValue(),
format_hints.Hex(cellroutine),
),
)
def run(self):
return renderers.TreeGrid(
[
("Hive Offset", renderers.format_hints.Hex),
("Hive Name", str),
("GetCellRoutine Module", str),
("GetCellRoutine Handler", renderers.format_hints.Hex),
],
self._generator(),
)
@@ -0,0 +1,141 @@
##
## plugin for testing addition of threads scan support to poolscanner.py
##
import logging
import datetime
from typing import Iterable
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import poolscanner
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Scans for windows threads."""
# version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags
_required_framework_version = (2, 6, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0)
),
]
@classmethod
def scan_threads(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Scans for threads using the poolscanner module and constraints.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
Returns:
A list of _ETHREAD objects found by scanning memory for the "Thre" / "Thr\\xE5" pool signatures
"""
constraints = poolscanner.PoolScanner.builtin_constraints(
symbol_table, [b"Thr\xe5", b"Thre"]
)
for result in poolscanner.PoolScanner.generate_pool_scan(
context, layer_name, symbol_table, constraints
):
_constraint, mem_object, _header = result
yield mem_object
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
for ethread in self.scan_threads(
self.context, kernel.layer_name, kernel.symbol_table_name
):
try:
thread_offset = ethread.vol.offset
owner_proc_pid = ethread.Cid.UniqueProcess
thread_tid = ethread.Cid.UniqueThread
thread_start_addr = ethread.StartAddress
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
except (ValueError, exceptions.InvalidAddressException):
vollog.debug(
"Thread :{}, invalid address {} in layer {}".format(
thread_tid, thread_start_addr, kernel.layer_name
)
)
continue
yield (
0,
(
format_hints.Hex(thread_offset),
owner_proc_pid,
thread_tid,
format_hints.Hex(thread_start_addr),
thread_create_time,
thread_exit_time,
),
)
def generate_timeline(self):
for row in self._generator():
_depth, row_data = row
row_dict = {}
(
row_dict["Offset"],
row_dict["PID"],
row_dict["TID"],
row_dict["StartAddress"],
row_dict["CreateTime"],
row_dict["ExitTime"],
) = row_data
# Skip threads with no creation time
# - mainly system process threads
if not isinstance(row_dict["CreateTime"], datetime.datetime):
continue
description = f"Thread: Tid {row_dict['TID']} in Pid {row_dict['PID']} (Offset {row_dict['Offset']})"
# yield created time, and if there is exit time, yield it too.
yield (description, timeliner.TimeLinerType.CREATED, row_dict["CreateTime"])
if isinstance(row_dict["ExitTime"], datetime.datetime):
yield (
description,
timeliner.TimeLinerType.MODIFIED,
row_dict["ExitTime"],
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("PID", int),
("TID", int),
("StartAddress", format_hints.Hex),
("CreateTime", datetime.datetime),
("ExitTime", datetime.datetime),
],
self._generator(),
)
@@ -78,7 +78,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
)
else:
raise exceptions.SymbolError(
None, module.name, "Required structures not found"
"SystemVaRegions", module.name, "Required structures not found"
)
elif module.has_symbol("MiSystemVaType"):
system_range_start = module.object(
@@ -99,7 +99,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
)
else:
raise exceptions.SymbolError(
None, module.name, "Required structures not found"
"MiVisibleState", module.name, "Required structures not found"
)
return result
+135 -7
View File
@@ -270,8 +270,8 @@
"d_tag": {
"offset": 0,
"type": {
"kind": "base",
"name": "long long"
"kind": "enum",
"name": "DtypeEnum64"
}
},
"d_ptr": {
@@ -699,8 +699,8 @@
"d_tag": {
"offset": 0,
"type": {
"kind": "base",
"name": "long"
"kind": "enum",
"name": "DtypeEnum32"
}
},
"d_ptr": {
@@ -905,11 +905,139 @@
"PT_PHDR": 6,
"PT_TLS": 7,
"PT_LOOS": 1610612736,
"PT_GNU_EH_FRAME": 1685382480,
"PT_GNU_STACK": 1685382481,
"PT_GNU_RELRO": 1685382482,
"PT_GNU_PROPERTY": 1685382483,
"PT_HIOS": 1879048191,
"PT_LOWPROC": 1879048192,
"PT_HIPROC": 2147483647
},
"size": 4
},
"DtypeEnum32": {
"base": "long",
"constants": {
"DT_NULL": 0,
"DT_NEEDED": 1,
"DT_PLTRELSZ": 2,
"DT_PLTGOT": 3,
"DT_HASH": 4,
"DT_STRTAB": 5,
"DT_SYMTAB": 6,
"DT_RELA": 7,
"DT_RELASZ": 8,
"DT_RELAENT": 9,
"DT_STRSZ": 10,
"DT_SYMENT": 11,
"DT_INIT": 12,
"DT_FINI": 13,
"DT_SONAME": 14,
"DT_RPATH": 15,
"DT_SYMBOLIC": 16,
"DT_REL": 17,
"DT_RELSZ": 18,
"DT_RELENT": 19,
"DT_PLTREL": 20,
"DT_DEBUG": 21,
"DT_TEXTREL": 22,
"DT_JMPREL": 23,
"DT_BIND_NOW": 24,
"DT_INIT_ARRAY": 25,
"DT_FINI_ARRAY": 26,
"DT_INIT_ARRAYSZ": 27,
"DT_FINI_ARRAYSZ": 28,
"DT_RUNPATH": 29,
"DT_FLAGS": 30,
"DT_ENCODING": 32,
"DT_PREINIT_ARRAYSZ": 33,
"DT_SYMTAB_SHNDX": 34,
"DT_RELRSZ": 35,
"DT_RELR": 36,
"DT_RELRENT": 37,
"DT_NUM": 38,
"OLD_DT_LOOS": 1610612736,
"DT_LOOS": 1610612749,
"DT_HIOS": 1879044096,
"DT_VALRNGLO": 1879047424,
"DT_VALRNGHI": 1879047679,
"DT_ADDRRNGLO": 1879047680,
"DT_GNU_HASH": 1879047925,
"DT_ADDRRNGHI": 1879047935,
"DT_VERSYM": 1879048176,
"DT_RELACOUNT": 1879048185,
"DT_RELCOUNT": 1879048186,
"DT_FLAGS_1": 1879048187,
"DT_VERDEF": 1879048188,
"DT_VERDEFNUM": 1879048189,
"DT_VERNEED": 1879048190,
"DT_VERNEEDNUM": 1879048191,
"DT_LOPROC": 1879048192,
"DT_HIPROC": 2147483647
},
"size": 4
},
"DtypeEnum64": {
"base": "long long",
"constants": {
"DT_NULL": 0,
"DT_NEEDED": 1,
"DT_PLTRELSZ": 2,
"DT_PLTGOT": 3,
"DT_HASH": 4,
"DT_STRTAB": 5,
"DT_SYMTAB": 6,
"DT_RELA": 7,
"DT_RELASZ": 8,
"DT_RELAENT": 9,
"DT_STRSZ": 10,
"DT_SYMENT": 11,
"DT_INIT": 12,
"DT_FINI": 13,
"DT_SONAME": 14,
"DT_RPATH": 15,
"DT_SYMBOLIC": 16,
"DT_REL": 17,
"DT_RELSZ": 18,
"DT_RELENT": 19,
"DT_PLTREL": 20,
"DT_DEBUG": 21,
"DT_TEXTREL": 22,
"DT_JMPREL": 23,
"DT_BIND_NOW": 24,
"DT_INIT_ARRAY": 25,
"DT_FINI_ARRAY": 26,
"DT_INIT_ARRAYSZ": 27,
"DT_FINI_ARRAYSZ": 28,
"DT_RUNPATH": 29,
"DT_FLAGS": 30,
"DT_ENCODING": 32,
"DT_PREINIT_ARRAYSZ": 33,
"DT_SYMTAB_SHNDX": 34,
"DT_RELRSZ": 35,
"DT_RELR": 36,
"DT_RELRENT": 37,
"DT_NUM": 38,
"OLD_DT_LOOS": 1610612736,
"DT_LOOS": 1610612749,
"DT_HIOS": 1879044096,
"DT_VALRNGLO": 1879047424,
"DT_VALRNGHI": 1879047679,
"DT_ADDRRNGLO": 1879047680,
"DT_GNU_HASH": 1879047925,
"DT_ADDRRNGHI": 1879047935,
"DT_VERSYM": 1879048176,
"DT_RELACOUNT": 1879048185,
"DT_RELCOUNT": 1879048186,
"DT_FLAGS_1": 1879048187,
"DT_VERDEF": 1879048188,
"DT_VERDEFNUM": 1879048189,
"DT_VERNEED": 1879048190,
"DT_VERNEEDNUM": 1879048191,
"DT_LOPROC": 1879048192,
"DT_HIPROC": 2147483647
},
"size": 8
}
},
"base_types": {
@@ -958,9 +1086,9 @@
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "ikelos-by-hand",
"datetime": "2019-10-21T22:52:00"
"version": "0.0.2",
"name": "gcmoreira-by-hand",
"datetime": "2024-02-19T14:37:00"
},
"format": "6.1.0"
}
@@ -7,14 +7,13 @@ import logging
import socket as socket_module
from typing import Generator, Iterable, Iterator, Optional, Tuple, List
from volatility3.framework import constants
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY
from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS
from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS
from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES
from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES
from volatility3.framework.constants.linux import CAPABILITIES
from volatility3.framework import exceptions, objects, interfaces, symbols
from volatility3.framework.layers import linear
from volatility3.framework.objects import utility
from volatility3.framework.symbols import generic, linux, intermed
@@ -707,7 +706,8 @@ class vm_area_struct(objects.StructType):
def get_page_offset(self) -> int:
if self.vm_file == 0:
return 0
return self.vm_pgoff << constants.linux.PAGE_SHIFT
parent_layer = self._context.layers[self.vol.layer_name]
return self.vm_pgoff << parent_layer.page_shift
def get_name(self, context, task):
if self.vm_file != 0:
@@ -736,7 +736,7 @@ class vm_area_struct(objects.StructType):
elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0:
ret = True
elif proclayer and "x" in flags_str:
for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT):
for i in range(self.vm_start, self.vm_end, proclayer.page_size):
try:
if proclayer.is_dirty(i):
vollog.warning(
@@ -1137,17 +1137,17 @@ class vfsmount(objects.StructType):
"""Helper to make sure it is comparing two pointers to 'vfsmount'.
Depending on the kernel version, the calling object (self) could be
a 'vfsmount \*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
a 'vfsmount \\*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
in the framework "auto" dereferencing ability to assure that when we
reach this point 'self' will be a 'vfsmount' already and self.vol.offset
a 'vfsmount \*' and not a 'vfsmount \*\*'. The argument must be a 'vfsmount \*'.
a 'vfsmount \\*' and not a 'vfsmount \\*\\*'. The argument must be a 'vfsmount \\*'.
Typically, it's called from do_get_path().
Args:
vfsmount_ptr (vfsmount \*): A pointer to a 'vfsmount'
vfsmount_ptr (vfsmount *): A pointer to a 'vfsmount'
Raises:
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \*'
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \\*'
Returns:
bool: 'True' if the given argument points to the the same 'vfsmount'
@@ -6,6 +6,10 @@ from typing import Dict, Tuple
import logging
from volatility3.framework import constants
from volatility3.framework.constants.linux import (
ELF_IDENT,
ELF_CLASS,
)
from volatility3.framework import objects, interfaces, exceptions
vollog = logging.getLogger(__name__)
@@ -59,13 +63,15 @@ class elf(objects.StructType):
ei_class = self._context.object(
symbol_table_name + constants.BANG + "unsigned char",
layer_name=layer_name,
offset=object_info.offset + 0x4,
offset=object_info.offset + ELF_IDENT.EI_CLASS,
)
if ei_class == 1:
if ei_class == ELF_CLASS.ELFCLASS32:
self._type_prefix = "Elf32_"
elif ei_class == 2:
self._ei_class_size = 32
elif ei_class == ELF_CLASS.ELFCLASS64:
self._type_prefix = "Elf64_"
self._ei_class_size = 64
else:
raise ValueError(f"Unsupported ei_class value {ei_class}")
@@ -140,36 +146,137 @@ class elf(objects.StructType):
)
return section_headers
def get_link_maps(self, kernel_symbol_table_name):
"""Get the ELF link map objects for the given VMA address
Args:
kernel_symbol_table_name (str): Kernel symbol table name
Yields:
The ELF link map objects
"""
got_entry_size = self._ei_class_size // 8
elf_symbol_table = self.get_symbol_table_name()
link_maps_seen = set()
for phdr in self.get_program_headers():
try:
if phdr.p_type.description != "PT_DYNAMIC":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
for dsec in phdr.dynamic_sections():
try:
if dsec.d_tag.description != "DT_PLTGOT":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF dynamic section type: {dsec.d_tag}",
)
continue
got_start = dsec.d_ptr
# link_map is stored at the second GOT entry
link_map_addr = got_start + got_entry_size
# It needs the kernel symbol table to create a pointer
link_map_ptr = self._context.object(
kernel_symbol_table_name + constants.BANG + "pointer",
offset=link_map_addr,
layer_name=self.vol.layer_name,
)
if not link_map_ptr:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid ELF link map pointer at 0x{link_map_addr:x}",
)
continue
linkmap_symname = (
elf_symbol_table + constants.BANG + self._type_prefix + "LinkMap"
)
try:
link_map = self._context.object(
object_type=linkmap_symname,
offset=link_map_ptr,
layer_name=self.vol.layer_name,
)
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid ELF link map address at 0x{link_map_ptr:x}",
)
continue
while link_map and link_map.vol.offset != 0:
if link_map.vol.offset in link_maps_seen:
break
link_maps_seen.add(link_map.vol.offset)
yield link_map
try:
link_map = self._context.object(
object_type=linkmap_symname,
offset=link_map.l_next,
layer_name=self.vol.layer_name,
)
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"ELF link map linked list is corrupt at 0x{self.vol.offset:x}",
)
break
def _find_symbols(self):
dt_strtab = None
dt_symtab = None
dt_strent = None
for phdr in self.get_program_headers():
# Find PT_DYNAMIC segment
try:
# Find PT_DYNAMIC segment
if str(phdr.p_type.description) != "PT_DYNAMIC":
if phdr.p_type.description != "PT_DYNAMIC":
continue
except ValueError:
# If the p_type value is outside the ones declared in the enumeration, an
# exception is raised
return None
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
# This section contains pointers to the strtab, symtab, and strent sections
for dsec in phdr.dynamic_sections():
if dsec.d_tag == 5:
try:
dtag = dsec.d_tag.description
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF dynamic section type: {dsec.d_tag}",
)
continue
if dtag == "DT_STRTAB":
dt_strtab = dsec.d_ptr
elif dsec.d_tag == 6:
elif dtag == "DT_SYMTAB":
dt_symtab = dsec.d_ptr
elif dsec.d_tag == 11:
elif dtag == "DT_SYMENT":
# Size of the symtab symbol entry
dt_strent = dsec.d_ptr
break
if dt_strtab is None or dt_symtab is None or dt_strent is None:
if not (dt_strtab and dt_symtab and dt_strent):
return None
self._cached_symtab = dt_symtab
@@ -274,19 +381,31 @@ class elf_phdr(objects.StructType):
def get_vaddr(self):
offset = self.__getattr__("p_vaddr")
if self._parent_e_type == 3: # ET_DYN
offset = self._parent_offset + offset
try:
if self._parent_e_type.description == "ET_DYN":
offset = self._parent_offset + offset
except ValueError:
# Unknown ELF object file type. Anyway, if the ELF object file type is not a
# shared object (ET_DYN), the virtual address is 'p_vaddr'.
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF object type: {self._parent_e_type}",
)
return offset
def dynamic_sections(self):
# sanity check
try:
if str(self.p_type.description) != "PT_DYNAMIC":
if self.p_type.description != "PT_DYNAMIC":
return None
except ValueError:
# If the value is outside the ones declared in the enumeration, an
# exception is raised
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {self.p_type}",
)
return None
# the buffer of array starts at elf_base + our virtual address ( offset )
@@ -314,10 +433,30 @@ class elf_phdr(objects.StructType):
break
class elf_linkmap(objects.StructType):
def get_name(self):
try:
buf = self._context.layers.read(self.vol.layer_name, self.l_name, 256)
except exceptions.PagedInvalidAddressException:
# Protection against memory smear
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid l_name address for ELF link map at 0x{self.vol.offset:x}",
)
return None
idx = buf.find(b"\x00")
if idx != -1:
buf = buf[:idx]
return buf.decode()
class_types = {
"Elf": elf,
"Elf64_Phdr": elf_phdr,
"Elf32_Phdr": elf_phdr,
"Elf32_Sym": elf_sym,
"Elf64_Sym": elf_sym,
"Elf32_LinkMap": elf_linkmap,
"Elf64_LinkMap": elf_linkmap,
}
@@ -21,12 +21,14 @@ class MacKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("vm_map_object", extensions.vm_map_object)
self.set_type_class("socket", extensions.socket)
self.set_type_class("inpcb", extensions.inpcb)
self.set_type_class("queue_entry", extensions.queue_entry)
self.set_type_class("ifnet", extensions.ifnet)
self.set_type_class("sockaddr_dl", extensions.sockaddr_dl)
self.set_type_class("sockaddr", extensions.sockaddr)
self.set_type_class("sysctl_oid", extensions.sysctl_oid)
self.set_type_class("kauth_scope", extensions.kauth_scope)
# https://developer.apple.com/documentation/kernel/queue_head_t
self.set_type_class("queue_entry", extensions.queue_entry)
self.optional_set_type_class("queue_head_t", extensions.queue_entry)
class MacUtilities(interfaces.configuration.VersionableInterface):
@@ -490,22 +490,24 @@ class queue_entry(objects.StructType):
for attr in ["next", "prev"]:
with contextlib.suppress(exceptions.InvalidAddressException):
n = getattr(self, attr).dereference().cast(type_name)
while n is not None and n.vol.offset != list_head:
if n.vol.offset in seen:
queue_element = getattr(self, attr).dereference().cast(type_name)
while (
queue_element is not None
and queue_element.vol.offset != list_head.vol.offset
):
if queue_element.vol.offset in seen:
break
yield n
yield queue_element
seen.add(n.vol.offset)
seen.add(queue_element.vol.offset)
yielded = yielded + 1
if yielded == max_size:
return None
n = (
getattr(n.member(attr=member_name), attr)
queue_element = (
getattr(queue_element.member(attr=member_name), attr)
.dereference()
.cast(type_name)
)
@@ -492,9 +492,47 @@ class KMUTANT(objects.StructType, pool.ExecutiveObject):
return header.NameInfo.Name.String # type: ignore
class ETHREAD(objects.StructType):
class ETHREAD(objects.StructType, pool.ExecutiveObject):
"""A class for executive thread objects."""
def is_valid(self) -> bool:
"""Determine if the object is valid."""
try:
# validation by TID:
if self.Cid.UniqueThread % 4 != 0: # NT tids are divisible by 4
return False
# validation by PID of parent process:
if self.Cid.UniqueProcess % 4 != 0:
return False
# validation by thread creation time:
if (
self.Cid.UniqueProcess != 4
): # The System process (PID 4) has no create time
ctime = self.get_create_time()
if not isinstance(ctime, datetime.datetime):
return False
if not (1998 < ctime.year < 2030):
return False
except exceptions.InvalidAddressException:
return False
# passed all validations
return True
def get_create_time(self):
# For Windows XPs
if self.has_member("ThreadsProcess"):
return conversion.wintime_to_datetime(self.CreateTime.QuadPart >> 3)
return conversion.wintime_to_datetime(self.CreateTime.QuadPart)
def get_exit_time(self):
return conversion.wintime_to_datetime(self.ExitTime.QuadPart)
def owning_process(self) -> interfaces.objects.ObjectInterface:
"""Return the EPROCESS that owns this thread."""
@@ -0,0 +1,131 @@
{
"base_types": {
"pointer": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned char": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 1
},
"unsigned long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 4
},
"unsigned long long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned short": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 2
}
},
"enums": {},
"metadata": {
"format": "6.1.0",
"producer": {
"datetime": "2021-07-31T17:37:28.313255",
"name": "vmextract-by-hand",
"version": "0.0.1"
}
},
"symbols": {
"revision_id": {
"address": 0,
"constant_data": "MTg="
}
},
"user_types": {
"_VMCS": {
"fields": {
"ept": {
"offset": 320,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"executive_vmcs_ptr": {
"offset": 208,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr3": {
"offset": 528,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr4": {
"offset": 536,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_pdpte": {
"offset": 544,
"type": {
"count": 4,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "unsigned long long"
}
}
},
"guest_physical_addr": {
"offset": 328,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr3": {
"offset": 816,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr4": {
"offset": 824,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vmcs_link_ptr": {
"offset": 248,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vpid": {
"offset": 206,
"type": {
"kind": "struct",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 4096
}
}
}
@@ -0,0 +1,131 @@
{
"base_types": {
"pointer": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned char": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 1
},
"unsigned long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 4
},
"unsigned long long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned short": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 2
}
},
"enums": {},
"metadata": {
"format": "6.1.0",
"producer": {
"datetime": "2021-07-16T16:21:01.062423",
"name": "vmextract-by-hand",
"version": "0.0.1"
}
},
"symbols": {
"revision_id": {
"address": 0,
"constant_data": "NA=="
}
},
"user_types": {
"_VMCS": {
"fields": {
"ept": {
"offset": 320,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"executive_vmcs_ptr": {
"offset": 208,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr3": {
"offset": 528,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr4": {
"offset": 536,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_pdpte": {
"offset": 544,
"type": {
"count": 4,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "unsigned long long"
}
}
},
"guest_physical_addr": {
"offset": 328,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr3": {
"offset": 816,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr4": {
"offset": 824,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vmcs_link_ptr": {
"offset": 248,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vpid": {
"offset": 206,
"type": {
"kind": "struct",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 4096
}
}
}