Merge branch 'feature/linux_ifconfig_plugin' into linux_ifconfig_plugin

This commit is contained in:
ikelos
2025-02-16 21:40:36 +00:00
committed by GitHub
43 changed files with 6454 additions and 302 deletions
+41
View File
@@ -867,6 +867,47 @@ def test_linux_ip_link(image, volatility, python):
assert rc == 0
def test_linux_kallsyms(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.kallsyms.Kallsyms",
image,
volatility,
python,
pluginargs=["--modules"],
)
# linux-sample-1.bin has no hidden modules.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") > 1000
# Addr Type Size Exported SubSystem ModuleName SymbolName Description
# 0xffffa009eba9 t 28 False module usbcore usb_mon_register Symbol is in the text (code) section
assert re.search(
rb"0xffffa009eba9\s+t\s+28\s+False\s+module\s+usbcore\s+usb_mon_register\s+Symbol is in the text \(code\) section",
out,
)
def test_linux_pscallstack(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.pscallstack.PsCallStack",
image,
volatility,
python,
pluginargs=["--pid", "1"],
)
assert rc == 0
assert out.count(b"\n") > 30
# TID Comm Position Address Value Name Type Module
# 1 init 39 0x88001f999a40 0xffff81109039 do_select T kernel
assert re.search(
rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel",
out,
)
# MAC
+1 -1
View File
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 19 # Number of changes that only add to the interface
VERSION_MINOR = 21 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -400,6 +400,27 @@ MODULE_MINIMUM_SIZE = 4096
# Kallsyms
KSYM_NAME_LEN = 512
NM_TYPES_DESC = {
"a": "Symbol is absolute and doesn't change during linking",
"b": "Symbol in the BSS section, typically holding zero-initialized or uninitialized data",
"c": "Symbol is common, typically holding uninitialized data",
"d": "Symbol is in the initialized data section",
"g": "Symbol is in an initialized data section for small objects",
"i": "Symbol is an indirect reference to another symbol",
"N": "Symbol is a debugging symbol",
"n": "Symbol is in a non-data, non-code, non-debug read-only section",
"p": "Symbol is in a stack unwind section",
"r": "Symbol is in a read only data section",
"s": "Symbol is in an uninitialized or zero-initialized data section for small objects",
"t": "Symbol is in the text (code) section",
"U": "Symbol is undefined",
"u": "Symbol is a unique global symbol",
"V": "Symbol is a weak object, with a default value",
"v": "Symbol is a weak object",
"W": "Symbol is a weak symbol but not marked as a weak object symbol, with a default value",
"w": "Symbol is a weak symbol but not marked as a weak object symbol",
"?": "Symbol type is unknown",
}
# VMCOREINFO
VMCOREINFO_MAGIC = b"VMCOREINFO\x00"
+34 -9
View File
@@ -186,6 +186,31 @@ class Intel(linear.LinearlyMappedLayer):
Returns the translated entry value
"""
offset &= self.address_mask
if not (self.minimum_address <= offset <= self.maximum_address):
raise exceptions.InvalidAddressException(
offset, f"Address {offset:#x} outside virtual address range"
)
page_address = offset & self.page_mask
return self._translate_page(page_address)
@functools.lru_cache(maxsize=1024)
def _translate_page(self, page_address: int) -> int:
"""Translates a page address based on paging tables.
Args:
page_address: The page base address
Returns:
the translated entry value
"""
if page_address & ~self.page_mask != 0:
raise exceptions.InvalidAddressException(
page_address,
f"Invalid page address {page_address:#x}. The address must be aligned to the page size",
)
# Setup the entry and how far we are through the offset
# Position maintains the number of bits left to process
# We or with 0x1 to ensure our page_map_offset is always valid
@@ -193,11 +218,13 @@ class Intel(linear.LinearlyMappedLayer):
entry = self._initial_entry
if not (
self.minimum_address <= (offset & self.address_mask) <= self.maximum_address
self.minimum_address
<= (page_address & self.address_mask)
<= self.maximum_address
):
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
page_address,
position + 1,
entry,
"Entry outside virtual address range: " + hex(entry),
@@ -209,7 +236,7 @@ class Intel(linear.LinearlyMappedLayer):
if not self._page_is_valid(entry):
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
page_address,
position + 1,
entry,
"Page Fault at entry " + hex(entry) + " in table " + name,
@@ -225,7 +252,7 @@ class Intel(linear.LinearlyMappedLayer):
# Figure out how much of the offset we should be using
start = position
position -= size
index = self._mask(offset, start, position + 1) >> (position + 1)
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(
@@ -236,17 +263,15 @@ class Intel(linear.LinearlyMappedLayer):
if table is None:
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
page_address,
position + 1,
entry,
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Read the data for the next entry
entry_data = table[
(index << self._index_shift) : (index << self._index_shift)
+ self._entry_size
]
entry_data_start = index << self._index_shift
entry_data = table[entry_data_start : entry_data_start + self._entry_size]
if INTEL_TRANSLATION_DEBUGGING:
vollog.log(
+3 -2
View File
@@ -356,8 +356,9 @@ class String(PrimitiveObject, str):
),
**params,
)
if value.find("\x00") >= 0:
value = value[: value.find("\x00")]
index = value.find("\x00")
if index >= 0:
value = value[:index]
return value
class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy):
+94 -8
View File
@@ -29,9 +29,23 @@ def bswap_64(value: int) -> int:
def array_to_string(
array: "objects.Array", count: Optional[int] = None, errors: str = "replace"
) -> interfaces.objects.ObjectInterface:
"""Takes a volatility Array of characters and returns a string."""
array: "objects.Array",
count: Optional[int] = None,
errors: str = "replace",
block_size=32,
) -> str:
"""Takes a Volatility 'Array' of characters and returns a Python string.
Args:
array: The Volatility `Array` object containing character elements.
count: Optional maximum number of characters to convert. If None, the function
processes the entire array.
errors: Specifies error handling behavior for decoding, defaulting to "replace".
block_size: Reading block size. Defaults to 32
Returns:
A decoded string representation of the character array.
"""
# TODO: Consider checking the Array's target is a native char
if not isinstance(array, objects.Array):
raise TypeError("Array_to_string takes an Array of char")
@@ -39,19 +53,91 @@ def array_to_string(
if count is None:
count = array.vol.count
return array.cast("string", max_length=count, errors=errors)
return address_to_string(
context=array._context,
layer_name=array.vol.layer_name,
address=array.vol.offset,
count=count,
errors=errors,
block_size=block_size,
)
def pointer_to_string(pointer: "objects.Pointer", count: int, errors: str = "replace"):
"""Takes a volatility Pointer to characters and returns a string."""
def pointer_to_string(
pointer: "objects.Pointer",
count: int,
errors: str = "replace",
block_size=32,
) -> str:
"""Takes a Volatility 'Pointer' to characters and returns a Python string.
Args:
pointer: A `Pointer` object containing character elements.
count: Optional maximum number of characters to convert. If None, the function
processes the entire array.
errors: Specifies error handling behavior for decoding, defaulting to "replace".
block_size: Reading block size. Defaults to 32
Returns:
A decoded string representation of the data referenced by the pointer.
"""
if not isinstance(pointer, objects.Pointer):
raise TypeError("pointer_to_string takes a Pointer")
if count < 1:
raise ValueError("pointer_to_string requires a positive count")
char = pointer.dereference()
return char.cast("string", max_length=count, errors=errors)
return address_to_string(
context=pointer._context,
layer_name=pointer.vol.layer_name,
address=pointer,
count=count,
errors=errors,
block_size=block_size,
)
def address_to_string(
context: interfaces.context.ContextInterface,
layer_name: str,
address: int,
count: int,
errors: str = "replace",
block_size=32,
) -> str:
"""Reads a null-terminated string from a given specified memory address, processing
it in blocks for efficiency.
Args:
context: The context used to retrieve memory layers and symbol tables
layer_name: The name of the memory layer to read from
address: The address where the string is located in memory
count: The number of bytes to read
errors: The error handling scheme to use for encoding errors. Defaults to "replace"
block_size: Reading block size. Defaults to 32
Returns:
The decoded string extracted from memory.
"""
if not isinstance(address, int):
raise TypeError("Address must be a valid integer")
if count < 1:
raise ValueError("Count must be greater than 0")
layer = context.layers[layer_name]
text = b""
while len(text) < count:
current_block_size = min(count - len(text), block_size)
temp_text = layer.read(address + len(text), current_block_size)
idx = temp_text.find(b"\x00")
if idx != -1:
temp_text = temp_text[:idx]
text += temp_text
break
text += temp_text
return text.decode(errors=errors)
def array_of_pointers(
+1 -1
View File
@@ -177,7 +177,7 @@ class Elfs(plugins.PluginInterface):
name,
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
path,
path or renderers.NotAvailableValue(),
file_output,
),
)
@@ -0,0 +1,138 @@
# 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 typing import List, Union
from volatility3.framework import interfaces, renderers
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.constants import architectures
from volatility3.framework.symbols.linux import kallsyms
vollog = logging.getLogger(__name__)
class Kallsyms(plugins.PluginInterface):
"""Kallsyms symbols enumeration plugin.
If no arguments are provided, all symbols are included: core, modules, ftrace, and BPF.
Alternatively, you can use any combination of --core, --modules, --ftrace, and --bpf
to customize the output.
"""
_required_framework_version = (2, 19, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="Kallsyms", component=kallsyms.Kallsyms, version=(1, 0, 0)
),
requirements.BooleanRequirement(
name="core",
description="Include core symbols",
default=False,
optional=True,
),
requirements.BooleanRequirement(
name="modules",
description="Include module symbols",
default=False,
optional=True,
),
requirements.BooleanRequirement(
name="ftrace",
description="Include ftrace symbols",
default=False,
optional=True,
),
requirements.BooleanRequirement(
name="bpf",
description="Include BPF symbols",
default=False,
optional=True,
),
]
def _get_symbol_size(
self, kassymbol: kallsyms.KASSymbol
) -> Union[int, interfaces.renderers.BaseAbsentValue]:
# Symbol sizes are calculated using the address of the next non-aliased
# symbol or the end of the kernel text area _end/_etext. However, some kernel
# symbols live beyond that area. For these symbols, the size will be negative,
# resulting in incorrect values. Unfortunately, there isn't much that can be done
# in such cases.
# See comments on .init.scratch in arch/x86/kernel/vmlinux.lds.S for details
return kassymbol.size if kassymbol.size >= 0 else renderers.NotAvailableValue()
def _generator(self):
module_name = self.config["kernel"]
vmlinux = self.context.modules[module_name]
kas = kallsyms.Kallsyms(
context=self.context,
layer_name=vmlinux.layer_name,
module_name=module_name,
)
include_core = self.config.get("core", False)
include_modules = self.config.get("modules", False)
include_ftrace = self.config.get("ftrace", False)
include_bpf = self.config.get("bpf", False)
symbols_flags = (include_core, include_modules, include_ftrace, include_bpf)
if not any(symbols_flags):
include_core = include_modules = include_ftrace = include_bpf = True
symbol_generators = []
if include_core:
symbol_generators.append(kas.get_core_symbols())
if include_modules:
symbol_generators.append(kas.get_modules_symbols())
if include_ftrace:
symbol_generators.append(kas.get_ftrace_symbols())
if include_bpf:
symbol_generators.append(kas.get_bpf_symbols())
for symbols_generator in symbol_generators:
for kassymbol in symbols_generator:
# Symbol sizes are calculated using the address of the next non-aliased
# symbol or the end of the kernel text area _end/_etext. However, some kernel
# symbols are located beyond that area, which causes this method to fail for
# the last symbol, resulting in a negative size.
# See comments on .init.scratch in arch/x86/kernel/vmlinux.lds.S for details
symbol_size = self._get_symbol_size(kassymbol)
fields = (
format_hints.Hex(kassymbol.address),
kassymbol.type,
symbol_size,
kassymbol.exported,
kassymbol.subsystem,
kassymbol.module_name,
kassymbol.name,
kassymbol.type_description or renderers.NotAvailableValue(),
)
yield 0, fields
def run(self):
headers = [
("Addr", format_hints.Hex),
("Type", str),
("Size", int),
("Exported", bool),
("SubSystem", str),
("ModuleName", str),
("SymbolName", str),
("Description", str),
]
return renderers.TreeGrid(headers, self._generator())
@@ -72,28 +72,35 @@ class Kthreads(plugins.PluginInterface):
if task.has_member("worker_private"):
# kernels >= 5.17 e32cf5dfbe227b355776948b2c9b5691b84d1cbd
ktread_base_pointer = task.worker_private
kthread_base_pointer = task.worker_private
else:
# 5.8 <= kernels < 5.17 in 52782c92ac85c4e393eb4a903a62e6c24afa633f threadfn
# was added to struct kthread. task.set_child_tid is safe on those versions.
ktread_base_pointer = task.set_child_tid
kthread_base_pointer = task.set_child_tid
if not ktread_base_pointer.is_readable():
if not kthread_base_pointer.is_readable():
continue
kthread = ktread_base_pointer.dereference().cast("kthread")
kthread = kthread_base_pointer.dereference().cast("kthread")
threadfn = kthread.threadfn
if not (threadfn and threadfn.is_readable()):
continue
task_name = utility.array_to_string(task.comm)
thread_name = task_name
# kernels >= 5.17 in d6986ce24fc00b0638bd29efe8fb7ba7619ed2aa full_name was added to kthread
thread_name = (
utility.pointer_to_string(kthread.full_name, count=255)
if kthread.has_member("full_name")
else task_name
)
if kthread.has_member("full_name"):
try:
thread_name = utility.pointer_to_string(
kthread.full_name, count=255
)
except exceptions.InvalidAddressException:
vollog.debug(
f"full_name pointer for thread at {kthread.vol.offset:#x} is paged out."
)
module_name, symbol_name = (
linux_utilities_modules.Modules.lookup_module_address(
self.context, vmlinux.name, handlers, threadfn
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import List
from typing import List, Tuple, Optional
import logging
from volatility3.framework import interfaces
from volatility3.framework import renderers, symbols
@@ -39,7 +39,9 @@ class Malfind(interfaces.plugins.PluginInterface):
),
]
def _list_injections(self, task):
def _list_injections(
self, task
) -> Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes]:
"""Generate memory regions for a process that may contain injected
code."""
@@ -54,12 +56,9 @@ class Malfind(interfaces.plugins.PluginInterface):
vollog.debug(
f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}"
)
if (
vma.is_suspicious(proc_layer)
and vma.get_name(self.context, task) != "[vdso]"
):
if vma.is_suspicious(proc_layer) and vma_name != "[vdso]":
data = proc_layer.read(vma.vm_start, 64, pad=True)
yield vma, data
yield vma, vma_name, data
def _generator(self, tasks):
# determine if we're on a 32 or 64 bit kernel
@@ -71,7 +70,7 @@ class Malfind(interfaces.plugins.PluginInterface):
for task in tasks:
process_name = utility.array_to_string(task.comm)
for vma, data in self._list_injections(task):
for vma, vma_name, data in self._list_injections(task):
if is_32bit_arch:
architecture = "intel"
else:
@@ -88,6 +87,7 @@ class Malfind(interfaces.plugins.PluginInterface):
process_name,
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
vma_name or renderers.NotAvailableValue(),
vma.get_protection(),
format_hints.HexBytes(data),
disasm,
@@ -103,6 +103,7 @@ class Malfind(interfaces.plugins.PluginInterface):
("Process", str),
("Start", format_hints.Hex),
("End", format_hints.Hex),
("Path", str),
("Protection", str),
("Hexdump", format_hints.HexBytes),
("Disasm", interfaces.renderers.Disassembly),
@@ -93,11 +93,14 @@ class MountInfo(plugins.PluginInterface):
return None
mnt_root_path = mnt_root.path()
superblock = mnt.get_mnt_sb()
mnt_id: int = mnt.mnt_id
parent_id: int = mnt.mnt_parent.mnt_id
superblock = mnt.get_mnt_sb()
if not (superblock and superblock.is_readable()):
return None
st_dev = f"{superblock.major}:{superblock.minor}"
mnt_opts: List[str] = []
+333 -40
View File
@@ -5,10 +5,15 @@
import math
import logging
import datetime
import time
import tarfile
from dataclasses import dataclass, astuple
from typing import List, Set, Type, Iterable, Tuple
from typing import IO, List, Set, Type, Iterable, Tuple
from io import BytesIO
from pathlib import PurePath
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.constants import architectures
from volatility3.framework import constants, renderers, interfaces, exceptions
from volatility3.framework.renderers import format_hints
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
@@ -37,6 +42,11 @@ class InodeUser:
modification_time: str
change_time: str
path: str
inode_size: int
@classmethod
def format_symlink(cls, symlink_source: str, symlink_dest: str) -> str:
return f"{symlink_source} -> {symlink_dest}"
@dataclass
@@ -80,6 +90,7 @@ class InodeInternal:
access_time_dt = self.inode.get_access_time()
modification_time_dt = self.inode.get_modification_time()
change_time_dt = self.inode.get_change_time()
inode_size = int(self.inode.i_size)
inode_user = InodeUser(
superblock_addr=superblock_addr,
@@ -95,6 +106,7 @@ class InodeInternal:
modification_time=modification_time_dt,
change_time=change_time_dt,
path=self.path,
inode_size=inode_size,
)
return inode_user
@@ -104,7 +116,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 3)
_version = (1, 1, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -112,7 +124,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0)
@@ -154,10 +166,10 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
and inode.i_link
and inode.i_link.is_readable()
):
i_link_str = inode.i_link.dereference().cast(
symlink_dest = inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
symlink_path = f"{symlink_path} -> {i_link_str}"
symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest)
return symlink_path
@@ -218,12 +230,14 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
follow_symlinks: bool = True,
) -> Iterable[InodeInternal]:
"""Retrieves the inodes from the superblocks
Args:
context: The context that the plugin will operate within
vmlinux_module_name: The name of the kernel module on which to operate
follow_symlinks: Whether to follow symlinks or not
Yields:
An InodeInternal object
@@ -303,7 +317,8 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
continue
seen_inodes.add(file_inode_ptr)
file_path = cls._follow_symlink(file_inode_ptr, file_path)
if follow_symlinks:
file_path = cls._follow_symlink(file_inode_ptr, file_path)
inode_in = InodeInternal(
superblock=superblock,
mountpoint=mountpoint,
@@ -393,6 +408,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
("ModificationTime", datetime.datetime),
("ChangeTime", datetime.datetime),
("FilePath", str),
("InodeSize", int),
]
return renderers.TreeGrid(
@@ -405,7 +421,7 @@ class InodePages(plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (2, 0, 2)
_version = (3, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -413,7 +429,7 @@ class InodePages(plugins.PluginInterface):
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="files", plugin=Files, version=(1, 0, 0)
@@ -439,62 +455,82 @@ class InodePages(plugins.PluginInterface):
@classmethod
def write_inode_content_to_file(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
inode: interfaces.objects.ObjectInterface,
filename: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
) -> None:
"""Extracts the inode's contents from the page cache and saves them to a file
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
inode: The inode to dump
filename: Filename for writing the inode content
open_method: class for constructing output files
vmlinux_layer: The kernel layer to obtain the page size
"""
try:
with open_method(filename) as file_obj:
cls.write_inode_content_to_stream(context, layer_name, inode, file_obj)
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
@classmethod
def write_inode_content_to_stream(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
inode: interfaces.objects.ObjectInterface,
stream: IO,
) -> None:
"""Extracts the inode's contents from the page cache and saves them to a stream
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
inode: The inode to dump
stream: An IO stream to write to, typically FileHandlerInterface or BytesIO
"""
if not inode.is_reg:
vollog.error("The inode is not a regular file")
return None
# By using truncate/seek, provided the filesystem supports it, a sparse file will be
layer = context.layers[layer_name]
# By using truncate/seek, provided the filesystem supports it, and the
# stream is a File interface, a sparse file will be
# created, saving both disk space and I/O time.
# Additionally, using the page index will guarantee that each page is written at the
# appropriate file position.
inode_size = inode.i_size
try:
file_initialized = False
with open_method(filename) as file_obj:
for page_idx, page_content in inode.get_contents():
current_fp = page_idx * vmlinux_layer.page_size
max_length = inode_size - current_fp
page_bytes_len = min(max_length, len(page_content))
if (
current_fp >= inode_size
or current_fp + page_bytes_len > inode_size
):
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.offset,
inode_size,
page_idx,
)
continue
page_bytes = page_content[:page_bytes_len]
stream_initialized = False
for page_idx, page_content in inode.get_contents():
current_fp = page_idx * layer.page_size
max_length = inode_size - current_fp
page_bytes_len = min(max_length, len(page_content))
if current_fp >= inode_size or current_fp + page_bytes_len > inode_size:
vollog.error(
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
inode.vol.offset,
inode_size,
page_idx,
)
continue
page_bytes = page_content[:page_bytes_len]
if not file_initialized:
# Lazy initialization to avoid truncating the file until we are
# certain there is something to write
file_obj.truncate(inode_size)
file_initialized = True
if not stream_initialized:
# Lazy initialization to avoid truncating the stream until we are
# certain there is something to write
stream.truncate(inode_size)
stream_initialized = True
file_obj.seek(current_fp)
file_obj.write(page_bytes)
stream.seek(current_fp)
stream.write(page_bytes)
except exceptions.LinuxPageCacheException:
vollog.error(
f"Error dumping cached pages for inode at {inode.vol.offset:#x}"
)
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
def _generate_inode_fields(
self,
@@ -575,7 +611,7 @@ class InodePages(plugins.PluginInterface):
filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp")
vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename)
self.write_inode_content_to_file(
inode, filename, open_method, vmlinux_layer
self.context, vmlinux_layer.name, inode, filename, open_method
)
else:
yield from self._generate_inode_fields(inode, vmlinux_layer)
@@ -593,3 +629,260 @@ class InodePages(plugins.PluginInterface):
return renderers.TreeGrid(
headers, Files.format_fields_with_headers(headers, self._generator())
)
class RecoverFs(plugins.PluginInterface):
"""Recovers the cached filesystem (directories, files, symlinks) into a compressed tarball.
Details: level 0 directories are named after the UUID of the parent superblock; metadata aren't replicated to extracted objects; objects modification time is set to the plugin run time; absolute symlinks
are converted to relative symlinks to prevent referencing the analyst's filesystem.
Troubleshooting: to fix extraction errors related to long paths, please consider using https://github.com/mxmlnkn/ratarmount.
"""
_version = (1, 0, 0)
_required_framework_version = (2, 21, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="files", plugin=Files, version=(1, 1, 0)
),
requirements.PluginRequirement(
name="inodepages", plugin=InodePages, version=(3, 0, 0)
),
requirements.ChoiceRequirement(
name="compression_format",
description="Compression format (default: gz)",
choices=["gz", "bz2", "xz"],
default="gz",
optional=True,
),
]
def _tar_add_reg_inode(
self,
context: interfaces.context.ContextInterface,
layer_name: str,
tar: tarfile.TarFile,
reg_inode_in: InodeInternal,
path_prefix: str = "",
mtime: float = None,
) -> int:
"""Extracts a REG inode content and writes it to a TarFile object.
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
tar: The TarFile object to write to
reg_inode_in: The inode to extract content from
path_prefix: A custom path prefix to prepend the inode path with
mtime: The modification time to set the TarInfo object to
Returns:
The number of extracted bytes
"""
inode_content_buffer = BytesIO()
InodePages.write_inode_content_to_stream(
context, layer_name, reg_inode_in.inode, inode_content_buffer
)
inode_content_buffer.seek(0)
handle_buffer_size = inode_content_buffer.getbuffer().nbytes
tar_info = tarfile.TarInfo(path_prefix + reg_inode_in.path)
# The tarfile module only has read support for sparse files:
# https://docs.python.org/3.12/library/tarfile.html#tarfile.LNKTYPE:~:text=and%20longlink%20extensions%2C-,read%2Donly%20support,-for%20all%20variants
tar_info.type = tarfile.REGTYPE
tar_info.size = handle_buffer_size
tar_info.mode = 0o444
if mtime is not None:
tar_info.mtime = mtime
tar.addfile(tar_info, inode_content_buffer)
return handle_buffer_size
def _tar_add_dir(
self,
tar: tarfile.TarFile,
directory_path: str,
mtime: float = None,
) -> None:
"""Adds a directory path to a TarFile object, based on a DIR inode.
Args:
tar: The TarFile object to write to
directory_path: The directory path to create
mtime: The modification time to set the TarInfo object to
"""
tar_info = tarfile.TarInfo(directory_path)
tar_info.type = tarfile.DIRTYPE
tar_info.mode = 0o755
if mtime is not None:
tar_info.mtime = mtime
tar.addfile(tar_info)
def _tar_add_lnk(
self,
tar: tarfile.TarFile,
symlink_source: str,
symlink_dest: str,
symlink_source_prefix: str = "",
mtime: float = None,
) -> None:
"""Adds a symlink to a TarFile object.
Args:
tar: The TarFile object to write to
symlink_source: The symlink source path
symlink_dest: The symlink target/destination
symlink_source_prefix: A custom path prefix to prepend the symlink source with
mtime: The modification time to set the TarInfo object to
"""
# Patch symlinks pointing to absolute paths,
# to prevent referencing the host filesystem.
if symlink_dest.startswith("/"):
relative_dest = PurePath(symlink_dest).relative_to(PurePath("/"))
# Remove the leading "/" to prevent an extra undesired "../" in the output
symlink_dest = (
PurePath(
*[".."] * len(PurePath(symlink_source.lstrip("/")).parent.parts)
)
/ relative_dest
).as_posix()
tar_info = tarfile.TarInfo(symlink_source_prefix + symlink_source)
tar_info.type = tarfile.SYMTYPE
tar_info.linkname = symlink_dest
tar_info.mode = 0o444
if mtime is not None:
tar_info.mtime = mtime
tar.addfile(tar_info)
def _generator(self):
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
tar_buffer = BytesIO()
tar = tarfile.open(
fileobj=tar_buffer,
mode=f"w:{self.config['compression_format']}",
)
# Set a unique timestamp for all extracted files
mtime = time.time()
inodes_iter = Files.get_inodes(
context=self.context,
vmlinux_module_name=vmlinux_module_name,
follow_symlinks=False,
)
# Prefix paths with the superblock UUID's to prevent overlaps.
# Switch to device major and device minor for older kernels (< 2.6.39-rc1).
uuid_as_prefix = vmlinux.get_type("super_block").has_member("s_uuid")
if not uuid_as_prefix:
vollog.warning(
"super_block struct does not support s_uuid attribute. Consequently, level 0 directories won't refer to the superblock uuid's, but to its device_major:device_minor numbers."
)
visited_paths = seen_prefixes = set()
for inode_in in inodes_iter:
# Code is slightly duplicated here with the if-block below.
# However this prevents unneeded tar manipulation if fifo
# or sock inodes come through for example.
if not (
inode_in.inode.is_reg or inode_in.inode.is_dir or inode_in.inode.is_link
):
continue
if not inode_in.path.startswith("/"):
vollog.debug(
f'Skipping processing of potentially smeared "{inode_in.path}" inode name as it does not starts with a "/".'
)
continue
# Construct the output path
if uuid_as_prefix:
prefix = f"/{inode_in.superblock.uuid}"
else:
prefix = f"/{inode_in.superblock.major}:{inode_in.superblock.minor}"
prefixed_path = prefix + inode_in.path
# Sanity check for already processed paths
if prefixed_path in visited_paths:
vollog.log(
constants.LOGLEVEL_VV,
f'Already processed prefixed inode path: "{prefixed_path}".',
)
continue
elif prefix not in seen_prefixes:
self._tar_add_dir(tar, prefix, mtime)
seen_prefixes.add(prefix)
visited_paths.add(prefixed_path)
extracted_file_size = renderers.NotApplicableValue()
# Inodes parent directory is yielded first, which
# ensures that a file parent path will exist beforehand.
# tarfile will take care of creating it anyway.
if inode_in.inode.is_reg:
extracted_file_size = self._tar_add_reg_inode(
self.context,
vmlinux_layer.name,
tar,
inode_in,
prefix,
mtime,
)
elif inode_in.inode.is_dir:
self._tar_add_dir(tar, prefixed_path, mtime)
elif (
inode_in.inode.is_link
and inode_in.inode.has_member("i_link")
and inode_in.inode.i_link
and inode_in.inode.i_link.is_readable()
):
symlink_dest = inode_in.inode.i_link.dereference().cast(
"string", max_length=255, encoding="utf-8", errors="replace"
)
self._tar_add_lnk(tar, inode_in.path, symlink_dest, prefix, mtime)
# Set path to a user friendly representation before yielding
inode_in.path = InodeUser.format_symlink(inode_in.path, symlink_dest)
else:
continue
inode_out = inode_in.to_user(vmlinux_layer)
yield (0, astuple(inode_out) + (extracted_file_size,))
tar.close()
tar_buffer.seek(0)
output_filename = f"recovered_fs.tar.{self.config['compression_format']}"
with self.open(output_filename) as f:
f.write(tar_buffer.getvalue())
def run(self):
headers = [
("SuperblockAddr", format_hints.Hex),
("MountPoint", str),
("Device", str),
("InodeNum", int),
("InodeAddr", format_hints.Hex),
("FileType", str),
("InodePages", int),
("CachedPages", int),
("FileMode", str),
("AccessTime", datetime.datetime),
("ModificationTime", datetime.datetime),
("ChangeTime", datetime.datetime),
("FilePath", str),
("InodeSize", int),
("Recovered FileSize", int),
]
return renderers.TreeGrid(
headers, Files.format_fields_with_headers(headers, self._generator())
)
+1 -1
View File
@@ -246,7 +246,7 @@ class Maps(plugins.PluginInterface):
major,
minor,
inode_num,
path,
path or renderers.NotAvailableValue(),
file_output,
),
)
@@ -0,0 +1,198 @@
# 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
import dataclasses
from typing import List, Iterator
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.renderers import format_hints
from volatility3.framework.constants import architectures
from volatility3.framework.objects import utility
from volatility3.framework.symbols.linux import kallsyms
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
@dataclasses.dataclass
class StackEntry:
position: int
address: int
value: int
name: str = renderers.NotAvailableValue()
type: str = renderers.NotAvailableValue()
module: str = renderers.NotAvailableValue()
class PsCallStack(plugins.PluginInterface):
"""Enumerates the call stack of each task"""
_required_framework_version = (2, 19, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="Kallsyms", component=kallsyms.Kallsyms, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="unresolved",
description="Include unresolved stack values",
default=False,
optional=True,
),
]
@classmethod
def get_task_callstack(
cls,
context: interfaces.context.ContextInterface,
module_name: str,
task: interfaces.objects.ObjectInterface,
kas: kallsyms.Kallsyms = None,
include_unresolved=False,
) -> Iterator[StackEntry]:
"""Retrieves the call stack for a given task
Args:
context: The context used to access memory layers and symbols
module_name: The name of the kernel module on which to operate
task: The task object whose stack is being retrieved
kas: Kallsyms instance for symbol resolution. If not provided or None, a new
instance will be created each time
include_unresolved: If True, includes stack values that could not be resolved
to known symbols. Defaults to False.
Yields:
StackEntry objects
"""
task_layer = task.get_address_space_layer()
if not task_layer:
return None
vmlinux = context.modules[module_name]
vmlinux_layer = context.layers[vmlinux.layer_name]
if not kas:
kas = kallsyms.Kallsyms(
context=context,
layer_name=vmlinux.layer_name,
module_name=module_name,
)
pointer_size = vmlinux.get_type("pointer").size
thread_size_order = 2 # Safe since kernel 3.15
# thread_size_order +=1 # If CONFIG_KASAN is enabled in kernels >= 4.0, default: DISABLED
# thread_size_order +=1 # If CONFIG_KASAN_EXTRA is enabled in kernels >= 4.19, default: DISABLED
thread_size = vmlinux_layer.page_size << thread_size_order
task_base_of_stack = vmlinux_layer.canonicalize(task.stack)
task_top_of_stack = task_base_of_stack + thread_size
byte_order = task.files.vol.data_format.byteorder
rsp_start = task.thread.sp
if not (task_base_of_stack <= rsp_start < task_top_of_stack):
raise exceptions.VolatilityException(
f"Invalid stack pointer {rsp_start:#x} for task {task.pid}"
)
current_sp = rsp_start
idx = 0
while current_sp < task_top_of_stack:
stack_value_bytes = task_layer.read(current_sp, pointer_size)
stack_value = int.from_bytes(stack_value_bytes, byteorder=byte_order)
kassymbol = kas.lookup_address(stack_value)
sp_address = current_sp & vmlinux_layer.address_mask
stack_value &= vmlinux_layer.address_mask
if kassymbol:
module_name = kassymbol.module_name or renderers.NotAvailableValue()
yield StackEntry(
position=idx,
address=sp_address,
value=stack_value,
name=kassymbol.name,
type=kassymbol.type,
module=module_name,
)
elif include_unresolved:
yield StackEntry(
position=idx,
address=sp_address,
value=stack_value,
)
idx += 1
current_sp += pointer_size
def _generator(self):
module_name = self.config["kernel"]
vmlinux = self.context.modules[module_name]
kas = kallsyms.Kallsyms(
context=self.context,
layer_name=vmlinux.layer_name,
module_name=self.config["kernel"],
)
include_unresolved = self.config.get("unresolved", False)
pids = self.config.get("pid", None)
filter_func = pslist.PsList.create_pid_filter(pids)
for task in pslist.PsList.list_tasks(
self.context, vmlinux.name, filter_func=filter_func, include_threads=True
):
task_name = utility.array_to_string(task.comm)
for stack_entry in self.get_task_callstack(
context=self.context,
module_name=vmlinux.name,
task=task,
kas=kas,
include_unresolved=include_unresolved,
):
fields = (
task.pid,
task_name,
stack_entry.position,
format_hints.Hex(stack_entry.address),
format_hints.Hex(stack_entry.value),
stack_entry.name,
stack_entry.type,
stack_entry.module,
)
yield 0, fields
def run(self):
return renderers.TreeGrid(
[
("TID", int),
("Comm", str),
("Position", int),
("Address", format_hints.Hex),
("Value", format_hints.Hex),
("Name", str),
("Type", str),
("Module", str),
],
self._generator(),
)
@@ -538,11 +538,14 @@ class Sockstat(plugins.PluginInterface):
continue
sock = socket.sk.dereference()
sock_type = sock.get_type()
family = sock.get_family()
try:
sock_type = sock.get_type()
family = sock.get_family()
sock_handler = SockHandlers(vmlinux, task)
sock_fields = sock_handler.process_sock(sock)
except exceptions.InvalidAddressException:
continue
sock_handler = SockHandlers(vmlinux, task)
sock_fields = sock_handler.process_sock(sock)
if not sock_fields:
continue
@@ -0,0 +1,315 @@
# 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
#
# Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf
import logging
from typing import Dict, List, Iterable, Optional
from enum import Enum
from dataclasses import dataclass
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.plugins.linux import hidden_modules, modxview
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid, NotAvailableValue
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import architectures
vollog = logging.getLogger(__name__)
# https://docs.python.org/3.13/library/enum.html#enum.IntFlag
class FtraceOpsFlags(Enum):
"""Denote the state of an ftrace_ops struct.
Based on https://elixir.bootlin.com/linux/v6.13-rc3/source/include/linux/ftrace.h#L255.
"""
FTRACE_OPS_FL_ENABLED = 1 << 0
FTRACE_OPS_FL_DYNAMIC = 1 << 1
FTRACE_OPS_FL_SAVE_REGS = 1 << 2
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED = 1 << 3
FTRACE_OPS_FL_RECURSION = 1 << 4
FTRACE_OPS_FL_STUB = 1 << 5
FTRACE_OPS_FL_INITIALIZED = 1 << 6
FTRACE_OPS_FL_DELETED = 1 << 7
FTRACE_OPS_FL_ADDING = 1 << 8
FTRACE_OPS_FL_REMOVING = 1 << 9
FTRACE_OPS_FL_MODIFYING = 1 << 10
FTRACE_OPS_FL_ALLOC_TRAMP = 1 << 11
FTRACE_OPS_FL_IPMODIFY = 1 << 12
FTRACE_OPS_FL_PID = 1 << 13
FTRACE_OPS_FL_RCU = 1 << 14
FTRACE_OPS_FL_TRACE_ARRAY = 1 << 15
FTRACE_OPS_FL_PERMANENT = 1 << 16
FTRACE_OPS_FL_DIRECT = 1 << 17
FTRACE_OPS_FL_SUBOP = 1 << 18
@dataclass
class ParsedFtraceOps:
"""Parsed ftrace_ops struct representation, containing a selection of forensics valuable
informations."""
ftrace_ops_offset: int
callback_symbol: str
callback_address: int
hooked_symbols: str
module_name: str
module_address: int
flags: str
class CheckFtrace(interfaces.plugins.PluginInterface):
"""Detect ftrace hooking
Investigate the ftrace infrastructure to uncover kernel attached callbacks, which can be leveraged
to hook kernel functions and modify their behaviour."""
_version = (1, 0, 0)
_required_framework_version = (2, 19, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 1, 0),
),
requirements.PluginRequirement(
name="modxview", plugin=modxview.Modxview, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="hidden_modules",
plugin=hidden_modules.Hidden_modules,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="show_ftrace_flags",
description="Show ftrace flags associated with an ftrace_ops struct",
optional=True,
default=False,
),
]
@classmethod
def extract_hash_table_filters(
cls,
ftrace_ops: interfaces.objects.ObjectInterface,
) -> Optional[Iterable[interfaces.objects.ObjectInterface]]:
"""Wrap the process of walking to every ftrace_func_entry of an ftrace_ops.
Those are stored in a hash table of filters that indicates the addresses hooked.
Args:
ftrace_ops: The ftrace_ops struct to walk through
Returns:
An iterable of ftrace_func_entry structs
"""
try:
current_bucket_ptr = ftrace_ops.func_hash.filter_hash.buckets.first
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VV,
f"ftrace_func_entry list of ftrace_ops@{ftrace_ops.vol.offset:#x} is empty/invalid. Skipping it...",
)
return []
while current_bucket_ptr.is_readable():
yield current_bucket_ptr.dereference().cast("ftrace_func_entry")
current_bucket_ptr = current_bucket_ptr.next
return None
@classmethod
def parse_ftrace_ops(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
known_modules: Dict[str, List[extensions.module]],
ftrace_ops: interfaces.objects.ObjectInterface,
run_hidden_modules: bool = True,
) -> Optional[Iterable[ParsedFtraceOps]]:
"""Parse an ftrace_ops struct to highlight ftrace kernel hooking.
Iterates over embedded ftrace_func_entry entries, which point to hooked memory areas.
Args:
known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through modxview.run_modules_scanners().
ftrace_ops: The ftrace_ops struct to parse
run_hidden_modules: Whether to run the hidden_modules plugin or not. Note: it won't be run, even if specified, \
if the "hidden_modules" key is present in known_modules.
Yields:
An iterable of ParsedFtraceOps dataclasses, containing a selection of useful fields (callback, hook, module) related to an ftrace_ops struct
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
callback = ftrace_ops.func
callback_symbol = module_address = module_name = None
# Try to lookup within the known modules if the callback address fits
module = linux_utilities_modules.Modules.module_lookup_by_address(
context,
kernel.layer_name,
modxview.Modxview.flatten_run_modules_results(known_modules),
callback,
)
# Run hidden_modules plugin if a callback origin couldn't be determined (only done once, results are re-used afterwards)
if (
module is None
and run_hidden_modules
and "hidden_modules" not in known_modules
):
vollog.info(
"A callback module origin could not be determined. hidden_modules plugin will be run to detect additional modules.",
)
known_modules_addresses = set(
kernel_layer.canonicalize(module.vol.offset)
for module in modxview.Modxview.flatten_run_modules_results(
known_modules
)
)
modules_memory_boundaries = (
hidden_modules.Hidden_modules.get_modules_memory_boundaries(
context, kernel_name
)
)
known_modules["hidden_modules"] = list(
hidden_modules.Hidden_modules.get_hidden_modules(
context,
kernel_name,
known_modules_addresses,
modules_memory_boundaries,
)
)
# Lookup the updated list to see if hidden_modules was able
# to find the missing module
module = linux_utilities_modules.Modules.module_lookup_by_address(
context,
kernel.layer_name,
modxview.Modxview.flatten_run_modules_results(known_modules),
callback,
)
# Fetch more information about the module
if module is not None:
module_address = module.vol.offset
module_name = module.get_name()
callback_symbol = module.get_symbol_by_address(callback)
else:
vollog.warning(
f"Could not determine ftrace_ops@{ftrace_ops.vol.offset:#x} callback {callback:#x} module origin.",
)
# Iterate over ftrace_func_entry list
for ftrace_func_entry in cls.extract_hash_table_filters(ftrace_ops):
hook_address = ftrace_func_entry.ip.cast("pointer")
# Determine the symbols associated with a hook
hooked_symbols = kernel.get_symbols_by_absolute_location(hook_address)
hooked_symbols = ",".join(
[
hooked_symbol.split(constants.BANG)[-1]
for hooked_symbol in hooked_symbols
]
)
formatted_ftrace_flags = ",".join(
[flag.name for flag in FtraceOpsFlags if flag.value & ftrace_ops.flags]
)
yield ParsedFtraceOps(
ftrace_ops.vol.offset,
callback_symbol,
callback,
hooked_symbols,
module_name,
module_address,
formatted_ftrace_flags,
)
return None
@classmethod
def iterate_ftrace_ops_list(
cls, context: interfaces.context.ContextInterface, kernel_name: str
) -> Optional[Iterable[interfaces.objects.ObjectInterface]]:
"""Iterate over (ftrace_ops *)ftrace_ops_list.
Returns:
An iterable of ftrace_ops structs
"""
kernel = context.modules[kernel_name]
current_frace_ops_ptr = kernel.object_from_symbol("ftrace_ops_list")
ftrace_list_end = kernel.object_from_symbol("ftrace_list_end")
while current_frace_ops_ptr.is_readable():
# ftrace_list_end is not considered a valid struct
# see kernel function test_rec_ops_needs_regs
if current_frace_ops_ptr != ftrace_list_end.vol.offset:
yield current_frace_ops_ptr.dereference()
current_frace_ops_ptr = current_frace_ops_ptr.next
else:
break
def _generator(self):
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
if not kernel.has_symbol("ftrace_ops_list"):
raise exceptions.SymbolError(
"ftrace_ops_list",
kernel.symbol_table_name,
'The provided symbol table does not include the "ftrace_ops_list" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupted.',
)
# Do not run hidden_modules by default, but only on failure to find a module
known_modules = modxview.Modxview.run_modules_scanners(
self.context, kernel_name, run_hidden_modules=False
)
for ftrace_ops in self.iterate_ftrace_ops_list(self.context, kernel_name):
for ftrace_ops_parsed in self.parse_ftrace_ops(
self.context,
kernel_name,
known_modules,
ftrace_ops,
):
formatted_results = (
format_hints.Hex(ftrace_ops_parsed.ftrace_ops_offset),
ftrace_ops_parsed.callback_symbol or NotAvailableValue(),
format_hints.Hex(ftrace_ops_parsed.callback_address),
ftrace_ops_parsed.hooked_symbols or NotAvailableValue(),
ftrace_ops_parsed.module_name or NotAvailableValue(),
(
format_hints.Hex(ftrace_ops_parsed.module_address)
if ftrace_ops_parsed.module_address is not None
else NotAvailableValue()
),
)
if self.config["show_ftrace_flags"]:
formatted_results += (ftrace_ops_parsed.flags,)
yield (0, formatted_results)
def run(self):
columns = [
("ftrace_ops address", format_hints.Hex),
("Callback", str),
("Callback address", format_hints.Hex),
("Hooked symbols", str),
("Module", str),
("Module address", format_hints.Hex),
]
if self.config.get("show_ftrace_flags"):
columns.append(("Flags", str))
return TreeGrid(
columns,
self._generator(),
)
@@ -0,0 +1,311 @@
# 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
#
# Public researches: https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Fixing-A-Memory-Forensics-Blind-Spot-Linux-Kernel-Tracing-wp.pdf
import logging
from typing import Dict, Iterable, List, Optional
from dataclasses import dataclass
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3.plugins.linux import hidden_modules, modxview
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, NotAvailableValue, TreeGrid
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.objects import utility
from volatility3.framework.constants import architectures
vollog = logging.getLogger(__name__)
@dataclass
class ParsedTracepointFunc:
"""Parsed tracepoint_func struct, containing a selection of forensics valuable
informations."""
tracepoint_name: str
tracepoint_address: int
probe_name: str
probe_address: int
probe_priority: int
module_name: str
module_address: int
class CheckTracepoints(interfaces.plugins.PluginInterface):
"""Detect tracepoints hooking
Investigate the tracepoints subsystem to uncover kernel attached probes, which can be leveraged
to hook kernel functions and modify their behaviour."""
_version = (1, 0, 0)
_required_framework_version = (2, 19, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(1, 1, 0),
),
requirements.PluginRequirement(
name="modxview", plugin=modxview.Modxview, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="hidden_modules",
plugin=hidden_modules.Hidden_modules,
version=(1, 0, 0),
),
]
@classmethod
def iterate_tracepoint_funcs(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
tracepoint: interfaces.objects.ObjectInterface,
) -> Optional[Iterable[interfaces.objects.ObjectInterface]]:
"""Extract probes represented by tracepoint_func structs from a
tracepoint funcs member.
Args:
tracepoint: The tracepoint struct to parse
Yields:
An iterable of tracepoint_func structs
"""
layer = context.layers[layer_name]
# Ignore tracepoints without attached probes
if not tracepoint.funcs.is_readable():
return None
current_tracepoint_func = tracepoint.funcs.dereference()
# Inspired by kernel's debug_print_probes()
while (
layer.is_valid(current_tracepoint_func.vol.offset)
and current_tracepoint_func.func.is_readable()
):
yield current_tracepoint_func
current_tracepoint_func = context.object(
tracepoint.get_symbol_table_name() + constants.BANG + "tracepoint_func",
layer_name,
current_tracepoint_func.vol.offset + current_tracepoint_func.vol.size,
)
@classmethod
def parse_tracepoint(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
known_modules: Dict[str, List[extensions.module]],
tracepoint: interfaces.objects.ObjectInterface,
run_hidden_modules: bool = True,
) -> Optional[Iterable[ParsedTracepointFunc]]:
"""Parse a tracepoint struct to highlight tracepoints kernel hooking.
Args:
known_modules: A dict of known modules, used to locate callbacks origin. Typically obtained through modxview.run_modules_scanners().
tracepoint: The tracepoint struct to parse
run_hidden_modules: Whether to run the hidden_modules plugin or not. Note: it won't be run, even if specified, \
if the "hidden_modules" key is present in known_modules.
Yields:
An iterable of ParsedTracepointFunc dataclasses, containing a selection of useful fields related to a tracepoint struct
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
for tracepoint_func in cls.iterate_tracepoint_funcs(
context, kernel_layer.name, tracepoint
):
probe_handler_address = tracepoint_func.func
probe_handler_symbol = module_address = module_name = None
# Try to lookup within the known modules if the probe_handler address fits
module = linux_utilities_modules.Modules.module_lookup_by_address(
context,
kernel.layer_name,
modxview.Modxview.flatten_run_modules_results(known_modules),
probe_handler_address,
)
# Run hidden_modules plugin if a probe handler origin couldn't be determined (only done once, results are re-used afterwards)
if (
module is None
and run_hidden_modules
and "hidden_modules" not in known_modules
):
vollog.info(
"A probe handler module origin could not be determined. hidden_modules plugin will be run to detect additional modules.",
)
known_modules_addresses = set(
kernel_layer.canonicalize(module.vol.offset)
for module in modxview.Modxview.flatten_run_modules_results(
known_modules
)
)
modules_memory_boundaries = (
hidden_modules.Hidden_modules.get_modules_memory_boundaries(
context, kernel_name
)
)
known_modules["hidden_modules"] = list(
hidden_modules.Hidden_modules.get_hidden_modules(
context,
kernel_name,
known_modules_addresses,
modules_memory_boundaries,
)
)
# Lookup the updated list to see if hidden_modules was able
# to find the missing module
module = linux_utilities_modules.Modules.module_lookup_by_address(
context,
kernel.layer_name,
modxview.Modxview.flatten_run_modules_results(known_modules),
probe_handler_address,
)
# Fetch more information about the module
if module is not None:
module_address = module.vol.offset
module_name = module.get_name()
probe_handler_symbol = module.get_symbol_by_address(
probe_handler_address
)
else:
vollog.warning(
f"Could not determine tracepoint@{tracepoint.vol.offset:#x} probe handler {probe_handler_address:#x} module origin.",
)
yield ParsedTracepointFunc(
utility.pointer_to_string(tracepoint.name, count=512),
tracepoint.vol.offset,
probe_handler_symbol,
probe_handler_address,
tracepoint_func.prio,
module_name,
module_address,
)
@classmethod
def iterate_tracepoints_array(
cls, context: interfaces.context.ContextInterface, kernel_name: str
) -> List[interfaces.objects.ObjectInterface]:
"""Iterate over (tracepoint_ptr_t *)__start___tracepoints_ptrs.
Handles CONFIG_HAVE_ARCH_PREL32_RELOCATIONS.
Returns:
A list of tracepoint structs
"""
kernel = context.modules[kernel_name]
tracepoints = []
tracepoints_start = kernel.object_from_symbol("__start___tracepoints_ptrs")
tracepoints_end = kernel.get_absolute_symbol_address(
"__stop___tracepoints_ptrs"
)
tracepoints_array_size = tracepoints_end - tracepoints_start.vol.offset
# kernel's tracepoint_ptr_deref() and tracepoint_ptr_t
# adjust depending on the use of PC-relative addressing
# or not.
# Relocation is commonly used to store pointers as offsets
# relative to their own address rather than absolute addresses/pointers.
config_have_arch_prel32_relocations = (
tracepoints_start.vol.subtype.type_name
== kernel.symbol_table_name + constants.BANG + "int"
)
if config_have_arch_prel32_relocations:
tracepoints_relative_offsets = tracepoints_start.cast(
"array",
count=tracepoints_array_size // kernel.get_type("int").size,
subtype=kernel.get_type("int"),
)
for relative_offset in tracepoints_relative_offsets:
# relative_offset is the value stored at relative_offset.vol.offset
# See kernel's offset_to_ptr(). Example:
# 0xffff9da125e0 = 0x7af138 + 0xffff9d2634a8
absolute_address = relative_offset + relative_offset.vol.offset
tracepoint = kernel.object(
"tracepoint",
absolute_address,
absolute=True,
)
tracepoints.append(tracepoint)
else:
tracepoints = utility.array_of_pointers(
tracepoints_start,
tracepoints_array_size // kernel.get_type("pointer").size,
kernel.symbol_table_name + constants.BANG + "tracepoint",
context,
)
return tracepoints
def _generator(self):
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
kernel_layer = self.context.layers[kernel.layer_name]
if not kernel.has_symbol("__start___tracepoints_ptrs"):
raise exceptions.SymbolError(
"__start___tracepoints_ptrs",
self.vmlinux.symbol_table_name,
'The provided symbol table does not include the "__start___tracepoints_ptrs" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupted.',
)
known_modules = modxview.Modxview.run_modules_scanners(
self.context, kernel_name, run_hidden_modules=False
)
tracepoints = self.iterate_tracepoints_array(self.context, kernel_name)
for tracepoint in tracepoints:
if not kernel_layer.is_valid(tracepoint.vol.offset):
continue
for tracepoint_parsed in self.parse_tracepoint(
self.context, kernel_name, known_modules, tracepoint
):
formatted_results = (
tracepoint_parsed.tracepoint_name,
format_hints.Hex(tracepoint_parsed.tracepoint_address),
tracepoint_parsed.probe_name or NotAvailableValue(),
format_hints.Hex(tracepoint_parsed.probe_address),
tracepoint_parsed.probe_priority,
tracepoint_parsed.module_name or NotAvailableValue(),
(
format_hints.Hex(tracepoint_parsed.module_address)
if tracepoint_parsed.module_address is not None
else NotAvailableValue()
),
)
yield (
0,
formatted_results,
)
def run(self):
columns = [
("tracepoint", str),
("tracepoint address", format_hints.Hex),
("Probe", str),
("Probe address", format_hints.Hex),
("Probe priority", int),
("Module", str),
("Module address", format_hints.Hex),
]
return TreeGrid(
columns,
self._generator(),
)
@@ -21,7 +21,7 @@ class BigPools(interfaces.plugins.PluginInterface):
"""List big page pools."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -66,7 +66,11 @@ class BigPools(interfaces.plugins.PluginInterface):
Yields:
A big page pool object
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
big_page_table_offset = ntkrnlmp.get_symbol("PoolBigPageTable").address
@@ -28,7 +28,7 @@ class Callbacks(interfaces.plugins.PluginInterface):
"""Lists kernel callbacks and notification routines."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -361,7 +361,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
A name, location and optional detail string
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
is_vista_or_later = versions.is_vista_or_later(
@@ -418,7 +422,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
Lists all registry callbacks from the old format via the CmpCallBackVector.
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
full_type_name = (
callback_table_name + constants.BANG + "_EX_CALLBACK_ROUTINE_BLOCK"
@@ -465,7 +473,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
Lists all registry callbacks via the CallbackListHead.
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
full_type_name = callback_table_name + constants.BANG + "_CM_CALLBACK_ENTRY"
@@ -506,7 +518,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
A name, location and optional detail string
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
if ntkrnlmp.has_symbol("CmpCallBackVector") and ntkrnlmp.has_symbol(
@@ -562,7 +578,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
A name, location and optional detail string
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
try:
@@ -626,7 +646,11 @@ class Callbacks(interfaces.plugins.PluginInterface):
A name, location and optional detail string
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
try:
@@ -70,6 +70,7 @@ class CmdLine(interfaces.plugins.PluginInterface):
for proc in procs:
process_name = utility.array_to_string(proc.ImageFileName)
proc_id = "Unknown"
result_text = None
try:
proc_id = proc.UniqueProcessId
@@ -78,13 +79,22 @@ class CmdLine(interfaces.plugins.PluginInterface):
)
except exceptions.SwappedInvalidAddressException as exp:
result_text = f"Required memory at {exp.invalid_address:#x} is inaccessible (swapped)"
vollog.debug(
f"Required memory at {exp.invalid_address:#x} is inaccessible (swapped)"
)
except exceptions.PagedInvalidAddressException as exp:
result_text = f"Required memory at {exp.invalid_address:#x} is not valid (process exited?)"
vollog.debug(
f"Required memory at {exp.invalid_address:#x} is not valid (process exited?)"
)
except exceptions.InvalidAddressException as exp:
result_text = f"Process {proc_id}: Required memory at {exp.invalid_address:#x} is not valid (incomplete layer {exp.layer_name}?)"
vollog.debug(
f"Process {proc_id}: Required memory at {exp.invalid_address:#x} is not valid (incomplete layer {exp.layer_name}?)"
)
if not result_text:
result_text = renderers.UnreadableValue()
yield (0, (proc.UniqueProcessId, process_name, result_text))
@@ -44,14 +44,16 @@ class DumpFiles(interfaces.plugins.PluginInterface):
description="Process ID to include (all other processes are excluded)",
optional=True,
),
requirements.IntRequirement(
requirements.ListRequirement(
name="virtaddr",
description="Dump a single _FILE_OBJECT at this virtual address",
element_type=int,
description="Dump the _FILE_OBJECTs at the given virtual address(es)",
optional=True,
),
requirements.IntRequirement(
requirements.ListRequirement(
name="physaddr",
description="Dump a single _FILE_OBJECT at this physical address",
element_type=int,
description="Dump a single _FILE_OBJECTs at the given physical address(es)",
optional=True,
),
requirements.StringRequirement(
@@ -318,24 +320,26 @@ class DumpFiles(interfaces.plugins.PluginInterface):
)
elif offsets:
virtual_layer_name = kernel.layer_name
# FIXME - change this after standard access to physical layer
physical_layer_name = self.context.layers[virtual_layer_name].config[
"memory_layer"
]
# Now process any offsets explicitly requested by the user.
for offset, is_virtual in offsets:
try:
layer_name = kernel.layer_name
# switch to a memory layer if the user provided --physaddr instead of --virtaddr
if not is_virtual:
layer_name = self.context.layers[layer_name].config[
"memory_layer"
]
file_obj = self.context.object(
kernel.symbol_table_name + constants.BANG + "_FILE_OBJECT",
layer_name=layer_name,
native_layer_name=kernel.layer_name,
layer_name=(
virtual_layer_name if is_virtual else physical_layer_name
),
native_layer_name=virtual_layer_name,
offset=offset,
)
for result in self.process_file_object(
self.context, kernel.layer_name, self.open, file_obj
self.context, virtual_layer_name, self.open, file_obj
):
yield (0, result)
except exceptions.InvalidAddressException:
@@ -355,11 +359,15 @@ class DumpFiles(interfaces.plugins.PluginInterface):
):
raise ValueError("Cannot use filter flag with an address flag")
if self.config.get("virtaddr", None) is not None:
offsets.append((self.config["virtaddr"], True))
elif self.config.get("physaddr", None) is not None:
offsets.append((self.config["physaddr"], False))
else:
if self.config.get("virtaddr"):
for virtaddr in self.config["virtaddr"]:
offsets.append((virtaddr, True))
if self.config.get("physaddr"):
for physaddr in self.config["physaddr"]:
offsets.append((physaddr, False))
if not offsets:
filter_func = pslist.PsList.create_pid_filter(
[self.config.get("pid", None)]
)
+11 -17
View File
@@ -67,24 +67,20 @@ class Envars(interfaces.plugins.PluginInterface):
symbol_table=kernel.symbol_table_name,
hive_offsets=None,
):
sys = False
ntuser = False
## The global variables
sys = None
try:
key = hive.get_key(
sys = hive.get_key(
"CurrentControlSet\\Control\\Session Manager\\Environment"
)
sys = True
except (KeyError, registry.RegistryFormatException):
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key(
sys = hive.get_key(
"ControlSet001\\Control\\Session Manager\\Environment"
)
sys = True
if sys:
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
for node in sys.get_values():
try:
value_node_name = node.get_name()
if value_node_name:
@@ -99,13 +95,13 @@ class Envars(interfaces.plugins.PluginInterface):
)
continue
ntuser = None
## The user-specific variables
with contextlib.suppress(KeyError, registry.RegistryFormatException):
key = hive.get_key("Environment")
ntuser = True
ntuser = hive.get_key("Environment")
if ntuser:
with contextlib.suppress(KeyError, registry.RegistryFormatException):
for node in key.get_values():
for node in ntuser.get_values():
try:
value_node_name = node.get_name()
if value_node_name:
@@ -200,15 +196,13 @@ class Envars(interfaces.plugins.PluginInterface):
return values
def _generator(self, data):
silent_vars = []
if self.config.get("SILENT", None):
silent_vars = self._get_silent_vars()
silent_vars = self._get_silent_vars() if self.config.get("SILENT") else []
for task in data:
for var, val in task.environment_variables():
if self.config.get("silent", None):
if var in silent_vars:
continue
if var in silent_vars:
continue
yield (
0,
(
@@ -18,7 +18,7 @@ class Handles(interfaces.plugins.PluginInterface):
"""Lists process open handles."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -144,7 +144,11 @@ class Handles(interfaces.plugins.PluginInterface):
type_map: Dict[int, str] = {}
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
try:
@@ -202,7 +206,11 @@ class Handles(interfaces.plugins.PluginInterface):
except exceptions.SymbolError:
return None
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
return context.object(
symbol_table + constants.BANG + "unsigned int",
layer_name,
@@ -216,7 +224,7 @@ class Handles(interfaces.plugins.PluginInterface):
kernel = self.context.modules[self.config["kernel"]]
virtual = kernel.layer_name
kvo = self.context.layers[virtual].config["kernel_virtual_offset"]
kvo = kernel.offset
ntkrnlmp = self.context.module(
kernel.symbol_table_name, layer_name=virtual, offset=kvo
@@ -243,7 +251,12 @@ class Handles(interfaces.plugins.PluginInterface):
layer_object = self.context.layers[virtual]
masked_offset = offset & layer_object.maximum_address
for entry in table:
for i in range(len(table)):
try:
entry = table[i]
except exceptions.InvalidAddressException:
vollog.debug(f"Failed to get handle table entry at index {i}")
continue
# This triggered a backtrace in many testing samples
# in the level == 0 path
# The code above this calls `is_valid` on the `offset`
@@ -17,7 +17,7 @@ class Info(plugins.PluginInterface):
"""Show OS & kernel details of the memory sample being analyzed."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -68,7 +68,9 @@ class Info(plugins.PluginInterface):
if not isinstance(virtual_layer, layers.intel.Intel):
raise TypeError("Virtual Layer is not an intel layer")
kvo = virtual_layer.config["kernel_virtual_offset"]
kvo = virtual_layer.config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError("Intel layer has no kernel virtual offset defined")
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
return ntkrnlmp
@@ -166,7 +168,9 @@ class Info(plugins.PluginInterface):
if not isinstance(virtual_layer, layers.intel.Intel):
raise TypeError("Virtual Layer is not an intel layer")
kvo = virtual_layer.config["kernel_virtual_offset"]
kvo = virtual_layer.config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError("Intel layer has no kernel virtual offset defined")
pe_table_name = intermed.IntermediateSymbolTable.create(
context,
@@ -4,7 +4,7 @@
import logging
from typing import Generator, Iterable, List, Optional
from volatility3.framework import constants, exceptions, interfaces, renderers
from volatility3.framework import symbols, constants, exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
@@ -18,7 +18,7 @@ class Modules(interfaces.plugins.PluginInterface):
"""Lists the loaded kernel modules."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 1, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -127,6 +127,32 @@ class Modules(interfaces.plugins.PluginInterface):
file_output,
)
@classmethod
def get_kernel_space_start(cls, context, module_name: str) -> int:
"""
Returns the starting address of the kernel address space
This method allows plugins that analyze kernel data structures to quickly detect
smeared or otherwise invalid data as many pointers must point into the kernel or
access during runtime would crash the system
"""
module = context.modules[module_name]
if symbols.symbol_table_is_64bit(context, module.symbol_table_name):
object_type = "unsigned long long"
else:
object_type = "unsigned long"
range_start_offset = module.get_symbol("MmSystemRangeStart").address
kernel_space_start = module.object(
object_type=object_type, offset=range_start_offset
)
layer = context.layers[module.layer_name]
return kernel_space_start & layer.address_mask
@classmethod
def get_session_layers(
cls,
@@ -247,7 +273,11 @@ class Modules(interfaces.plugins.PluginInterface):
A list of Modules as retrieved from PsLoadedModuleList
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
try:
@@ -5,9 +5,9 @@
import logging
from typing import List, Generator
from volatility3.framework import interfaces, symbols
from volatility3.framework import interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.plugins.windows import thrdscan, ssdt
from volatility3.plugins.windows import thrdscan, ssdt, modules
vollog = logging.getLogger(__name__)
@@ -37,6 +37,9 @@ class Threads(thrdscan.ThrdScan):
requirements.PluginRequirement(
name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="modules", plugin=modules.Modules, version=(2, 1, 0)
),
]
@classmethod
@@ -56,24 +59,27 @@ class Threads(thrdscan.ThrdScan):
"""
module = context.modules[module_name]
layer_name = module.layer_name
symbol_table = module.symbol_table_name
symbol_table_name = module.symbol_table_name
collection = ssdt.SSDT.build_module_collection(
context, layer_name, symbol_table
context, layer_name, symbol_table_name
)
# FIXME - use a proper constant once established
# used to filter out smeared pointers
if symbols.symbol_table_is_64bit(context, symbol_table):
kernel_start = 0xFFFFF80000000000
else:
kernel_start = 0x80000000
kernel_space_start = modules.Modules.get_kernel_space_start(
context, module_name
)
for thread in thrdscan.ThrdScan.scan_threads(context, module_name):
# we don't want smeared or terminated threads
# We don't want smeared or terminated threads
# So we access the owning process (which could also be terminated or smeared)
# Plus check the start address holding page
try:
proc = thread.owning_process()
except AttributeError:
pid = proc.UniqueProcessId
ppid = proc.InheritedFromUniqueProcessId
thread_start = thread.StartAddress
except (AttributeError, exceptions.InvalidAddressException):
continue
# we only care about kernel threads, 4 = System
@@ -81,14 +87,19 @@ class Threads(thrdscan.ThrdScan):
# such as bit fields and flags are not stable in Win10+
# so we check if the thread is from the kernel itself or one its child
# kernel processes (MemCompression, Regsitry, ...)
if proc.UniqueProcessId != 4 and proc.InheritedFromUniqueProcessId != 4:
if pid != 4 and ppid != 4:
continue
if thread.StartAddress < kernel_start:
# if the thread has an exit time or terminated (4) state, then skip it
if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4:
continue
# threads pointing into userland, which is from smeared or terminated threads
if thread_start < kernel_space_start:
continue
module_symbols = list(
collection.get_module_symbols_by_absolute_location(thread.StartAddress)
collection.get_module_symbols_by_absolute_location(thread_start)
)
# alert on threads that do not map to a module
@@ -244,7 +244,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 7, 0)
_version = (1, 0, 1)
_version = (1, 1, 0)
# used for special handling of the kernel PDB file. See later notes
os_module_name = "ntoskrnl.exe"
@@ -292,8 +292,9 @@ class PESymbols(interfaces.plugins.PluginInterface):
),
]
@staticmethod
def _get_pefile_obj(
@classmethod
def get_pefile_obj(
cls,
context: interfaces.context.ContextInterface,
pe_table_name: str,
layer_name: str,
@@ -486,7 +487,7 @@ class PESymbols(interfaces.plugins.PluginInterface):
module_start = module_info[1]
# we need a valid PE with an export table
pe_module = PESymbols._get_pefile_obj(
pe_module = PESymbols.get_pefile_obj(
context, pe_table_name, layer_name, module_start
)
if not pe_module:
@@ -22,7 +22,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
PHYSICAL_DEFAULT = False
@classmethod
@@ -226,7 +226,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""
# We only use the object factory to demonstrate how to use one
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
ps_aph_offset = ntkrnlmp.get_symbol("PsActiveProcessHead").address
@@ -23,7 +23,7 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Scans for processes present in a particular windows memory image."""
_required_framework_version = (2, 3, 1)
_version = (1, 1, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls):
@@ -194,9 +194,12 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# If it's WinXP->8.1 we have now a physical process address.
# We'll use the first thread to bounce back to the virtual process
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
tleoffset = ntkrnlmp.get_type("_ETHREAD").relative_child_offset(
"ThreadListEntry"
)
@@ -41,7 +41,7 @@ class HiveGenerator:
class HiveList(interfaces.plugins.PluginInterface):
"""Lists the registry hives present in a particular memory image."""
_version = (1, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 0, 0)
@classmethod
@@ -59,7 +59,7 @@ class HiveList(interfaces.plugins.PluginInterface):
default=None,
),
requirements.PluginRequirement(
name="hivescan", plugin=hivescan.HiveScan, version=(1, 0, 0)
name="hivescan", plugin=hivescan.HiveScan, version=(2, 0, 0)
),
requirements.BooleanRequirement(
name="dump",
@@ -215,7 +215,11 @@ class HiveList(interfaces.plugins.PluginInterface):
"""
# We only use the object factory to demonstrate how to use one
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
list_head = ntkrnlmp.get_symbol("CmpHiveListHead").address
@@ -278,9 +282,7 @@ class HiveList(interfaces.plugins.PluginInterface):
f"Hivelist failed traversing backwards at {hex(backward_invalid)}, a different "
"location from forwards, revert to scanning"
)
for hive in hivescan.HiveScan.scan_hives(
context, layer_name, symbol_table
):
for hive in hivescan.HiveScan.scan_hives(context, ntkrnlmp.name):
try:
if hive.HiveList.Flink:
start_hive_offset = hive.HiveList.Flink - reloff
@@ -15,7 +15,7 @@ class HiveScan(interfaces.plugins.PluginInterface):
"""Scans for registry hives present in a particular windows memory image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
@@ -35,10 +35,7 @@ class HiveScan(interfaces.plugins.PluginInterface):
@classmethod
def scan_hives(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
cls, context: interfaces.context.ContextInterface, kernel_name: str
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Scans for hives using the poolscanner module and constraints or bigpools module with tag.
@@ -51,17 +48,21 @@ class HiveScan(interfaces.plugins.PluginInterface):
A list of Hive objects as found from the `layer_name` layer based on Hive pool signatures
"""
is_64bit = symbols.symbol_table_is_64bit(context, symbol_table)
kernel = context.modules[kernel_name]
is_64bit = symbols.symbol_table_is_64bit(context, kernel.symbol_table_name)
is_windows_8_1_or_later = versions.is_windows_8_1_or_later(
context=context, symbol_table=symbol_table
context=context, symbol_table=kernel.symbol_table_name
)
if is_windows_8_1_or_later and is_64bit:
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
ntkrnlmp = kernel
for pool in bigpools.BigPools.list_big_pools(
context, layer_name=layer_name, symbol_table=symbol_table, tags=["CM10"]
context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
tags=["CM10"],
):
cmhive = ntkrnlmp.object(
object_type="_CMHIVE", offset=pool.Va, absolute=True
@@ -70,21 +71,17 @@ class HiveScan(interfaces.plugins.PluginInterface):
else:
constraints = poolscanner.PoolScanner.builtin_constraints(
symbol_table, [b"CM10"]
kernel.symbol_table_name, [b"CM10"]
)
for result in poolscanner.PoolScanner.generate_pool_scan(
context, layer_name, symbol_table, constraints
context, kernel.layer_name, kernel.symbol_table_name, constraints
):
_constraint, mem_object, _header = result
yield mem_object
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
for hive in self.scan_hives(
self.context, kernel.layer_name, kernel.symbol_table_name
):
for hive in self.scan_hives(self.context, self.config["kernel"]):
yield (0, (format_hints.Hex(hive.vol.offset),))
def run(self):
@@ -11,14 +11,13 @@
#
# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html
import io
import logging
from typing import Iterable, Tuple, List, Optional
import pefile
from volatility3.framework import interfaces, symbols, exceptions
from volatility3.framework import renderers, constants
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanners
from volatility3.framework.objects import utility
@@ -26,7 +25,7 @@ from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows import pdbutil
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins.windows import pslist, vadinfo
from volatility3.plugins.windows import pslist, vadinfo, pe_symbols
try:
import capstone
@@ -61,43 +60,11 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0)
),
requirements.VersionRequirement(
name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 1, 0)
),
]
def _get_pefile_obj(
self, pe_table_name: str, layer_name: str, base_address: int
) -> pefile.PE:
"""
Attempts to pefile object from the bytes of the PE file
Args:
pe_table_name: name of the pe types table
layer_name: name of the lsass.exe process layer
base_address: base address of cryptdll.dll in lsass.exe
Returns:
the constructed pefile object
"""
pe_data = io.BytesIO()
try:
dos_header = self.context.object(
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
offset=base_address,
layer_name=layer_name,
)
for offset, data in dos_header.reconstruct():
pe_data.seek(offset)
pe_data.write(data)
pe_ret = pefile.PE(data=pe_data.getvalue(), fast_load=True)
except exceptions.InvalidAddressException:
vollog.debug("Unable to reconstruct cryptdll.dll in memory")
pe_ret = None
return pe_ret
def _check_for_skeleton_key_vad(
self,
csystem: interfaces.objects.ObjectInterface,
@@ -497,7 +464,9 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
)
cryptdll = self._get_pefile_obj(pe_table_name, proc_layer_name, cryptdll_base)
cryptdll = pe_symbols.PESymbols.get_pefile_obj(
self.context, pe_table_name, proc_layer_name, cryptdll_base
)
if not cryptdll:
return None
@@ -19,7 +19,7 @@ class SSDT(plugins.PluginInterface):
"""Lists the system call table."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -89,10 +89,8 @@ class SSDT(plugins.PluginInterface):
self.context, layer_name, kernel.symbol_table_name
)
kvo = self.context.layers[layer_name].config["kernel_virtual_offset"]
ntkrnlmp = self.context.module(
kernel.symbol_table_name, layer_name=layer_name, offset=kvo
)
ntkrnlmp = kernel
kvo = kernel.offset
# this is just one way to enumerate the native (NT) service table.
# to do the same thing for the Win32K service table, we would need Win32K.sys symbol support
@@ -22,7 +22,7 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt
"""Lists the unloaded kernel modules."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -88,7 +88,11 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt
A list of Unloaded Modules as retrieved from MmUnloadedDrivers
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
unloadedmodules_offset = ntkrnlmp.get_symbol("MmUnloadedDrivers").address
unloadedmodules = ntkrnlmp.object(
@@ -117,7 +121,18 @@ class UnloadedModules(interfaces.plugins.PluginInterface, timeliner.TimeLinerInt
)
unloadedmodules_array.UnloadedDrivers.count = unloaded_count
yield from unloadedmodules_array.UnloadedDrivers
for driver in unloadedmodules_array.UnloadedDrivers:
# Mass testing led to dozens of samples backtracing on this plugin when
# accessing members of modules coming out this list
# Given how often temporary drivers load and unload on Win10+, I
# assume the chance for smear is very high
try:
driver.StartAddress
driver.EndAddress
driver.CurrentTime
yield driver
except exceptions.InvalidAddressException:
continue
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
@@ -34,7 +34,7 @@ class VadInfo(interfaces.plugins.PluginInterface):
"""Lists process memory ranges."""
_required_framework_version = (2, 4, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
def __init__(self, *args, **kwargs):
@@ -99,7 +99,11 @@ class VadInfo(interfaces.plugins.PluginInterface):
symbol_table: The name of the table containing the kernel symbols
"""
kvo = context.layers[layer_name].config["kernel_virtual_offset"]
kvo = context.layers[layer_name].config.get("kernel_virtual_offset", None)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo)
addr = ntkrnlmp.get_symbol("MmProtectToValue").address
values = ntkrnlmp.object(
@@ -17,6 +17,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
"""Lists virtual mapped sections."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -147,7 +148,7 @@ class VirtMap(interfaces.plugins.PluginInterface):
module = self.context.module(
kernel.symbol_table_name,
layer_name=layer.name,
offset=layer.config["kernel_virtual_offset"],
offset=kernel.offset,
)
return renderers.TreeGrid(
+58 -11
View File
@@ -7,7 +7,7 @@ import contextlib
import functools
import logging
from abc import ABC, abstractmethod
from typing import Iterator, List, Tuple, Optional, Union, Dict
from typing import List, Tuple, Optional, Union, Dict, Generator, Iterator
import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules
from volatility3 import framework
@@ -92,6 +92,9 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
# Only found in 6.1+ kernels
self.optional_set_type_class("maple_tree", extensions.maple_tree)
self.optional_set_type_class("latch_tree_root", extensions.latch_tree_root)
self.optional_set_type_class("kernel_symbol", extensions.kernel_symbol)
class LinuxUtilities(interfaces.configuration.VersionableInterface):
"""Class with multiple useful linux functions."""
@@ -278,7 +281,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
ns_ops = ns_common.ops
pre_name = utility.pointer_to_string(ns_ops.name, 255)
except IndexError:
except (exceptions.SymbolError, IndexError):
pre_name = "<unsupported ns_dname implementation>"
else:
pre_name = f"<unsupported d_op symbol> {sym}"
@@ -341,16 +344,16 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
symbol_table: str,
task: interfaces.objects.ObjectInterface,
):
# task.files can be null
if not (task.files and task.files.is_readable()):
return None
try:
files = task.files
fd_table = files.get_fds()
if fd_table == 0:
return None
fd_table = task.files.get_fds()
if fd_table == 0:
max_fds = files.get_max_fds()
except exceptions.InvalidAddressException:
return None
max_fds = task.files.get_max_fds()
# corruption check
if max_fds > 500000:
return None
@@ -434,14 +437,58 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
)
@classmethod
def walk_internal_list(cls, vmlinux, struct_name, list_member, list_start):
def walk_internal_list(
cls,
vmlinux: interfaces.context.ModuleInterface,
struct_name: str,
list_member: str,
list_start: interfaces.objects.ObjectInterface,
max_count: int = 4096,
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""
An API that provides generic, smear-resistant enumeration of embedded lists
Args:
vmlinux:
struct_name: name of the structure of the list elements
list_member: name of the list_member holding the internal list
list_start: Starting (head) member of the list
max_count: Optional maximum amount of list elements that will be yielded
Returns:
Instances of `struct_name`
"""
count = 0
seen = set()
while list_start:
if list_start.vol.offset in seen:
vollog.debug(
"walk_internal_list: Repeat entry found. Stopping enumeration"
)
break
seen.add(list_start.vol.offset)
if not (list_start and list_start.is_readable()):
break
list_struct = vmlinux.object(
object_type=struct_name, offset=list_start.vol.offset
object_type=struct_name, offset=list_start.vol.offset, absolute=True
)
yield list_struct
list_start = getattr(list_struct, list_member)
if count == max_count:
vollog.debug(
f"walk_internal_list: Breaking list enumeration at maximum allowed count of {count}"
)
break
count += 1
@classmethod
def container_of(
cls,
@@ -10,7 +10,18 @@ import binascii
import stat
import datetime
import socket as socket_module
from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union, Dict
import uuid
from typing import (
Generator,
Iterable,
Iterator,
Optional,
Tuple,
List,
Union,
Dict,
Callable,
)
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
from volatility3.framework.renderers import conversion, UnparsableValue
@@ -167,37 +178,43 @@ class module(generic.GenericIntelProcess):
"""Get the name of the module as a string"""
return utility.array_to_string(self.name)
def _get_sect_count(self, grp):
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(
self.get_symbol_table_name() + constants.BANG + "array",
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=grp.attrs,
subtype=self._context.symbol_space.get_type(
self.get_symbol_table_name() + constants.BANG + "pointer"
symbol_table_name + constants.BANG + "pointer"
),
count=25,
)
idx = 0
while arr[idx]:
while arr[idx] and arr[idx].is_readable():
idx = idx + 1
return idx
def get_sections(self):
"""Get sections of the module"""
@functools.cached_property
def number_of_sections(self) -> int:
if self.sect_attrs.has_member("nsections"):
num_sects = self.sect_attrs.nsections
else:
num_sects = self._get_sect_count(self.sect_attrs.grp)
return self.sect_attrs.nsections
return self._get_sect_count(self.sect_attrs.grp)
def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Get a list of section attributes for the given module."""
symbol_table_name = self.get_symbol_table_name()
arr = self._context.object(
self.get_symbol_table_name() + constants.BANG + "array",
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(
self.get_symbol_table_name() + constants.BANG + "module_sect_attr"
symbol_table_name + constants.BANG + "module_sect_attr"
),
count=num_sects,
count=self.number_of_sections,
)
yield from arr
@@ -255,6 +272,43 @@ class module(generic.GenericIntelProcess):
sym_address = elf_sym_obj.st_value & layer.address_mask
yield (sym_name, sym_address)
@functools.lru_cache
def get_module_address_boundaries(self) -> Tuple[int, int]:
"""Return the module address boundaries based on its symbol addresses"""
if not self.section_strtab or self.num_symtab < 1:
return None
elf_table_name = self.get_elf_table_name()
symbol_table_name = self.get_symbol_table_name()
is_64bit = symbols.symbol_table_is_64bit(self._context, symbol_table_name)
sym_name = "Elf64_Sym" if is_64bit else "Elf32_Sym"
sym_type = self._context.symbol_space.get_type(
elf_table_name + constants.BANG + sym_name
)
elf_syms = self._context.object(
symbol_table_name + constants.BANG + "array",
layer_name=self.vol.layer_name,
offset=self.section_symtab,
subtype=sym_type,
count=self.num_symtab,
)
# They should be sorted, but just in case
elf_syms_sorted = sorted(elf_syms, key=lambda x: x.st_value)
layer = self._context.layers[self.vol.layer_name]
# The first elf_sym is null
first_symbol = elf_syms_sorted[1]
last_symbol = elf_syms_sorted[-1]
minimum_address = first_symbol.st_value & layer.address_mask
maximum_address = (
last_symbol.st_value & layer.address_mask + last_symbol.st_size
)
return minimum_address, maximum_address
def get_symbol(self, wanted_sym_name) -> Optional[int]:
"""Get symbol address for a given symbol name"""
for sym_name, sym_address in self.get_symbols_names_and_addresses():
@@ -300,6 +354,38 @@ class module(generic.GenericIntelProcess):
raise AttributeError("Unable to get strtab")
@property
def section_typetab(self):
if self.has_member("kallsyms") and self.kallsyms.has_member("typetab"):
# kernels >= 4.5 8244062ef1e54502ef55f54cced659913f244c3e: kallsyms was added
# kernels >= 5.2 1c7651f43777cdd59c1aaa82c87324d3e7438c7b: types have its own array
return self.kallsyms.typetab
raise AttributeError("Unable to get typetab section, it needs a kernel >= 5.2")
def get_symbol_type(
self, symbol: interfaces.objects.ObjectInterface, symbol_index: int
) -> str:
"""Determines the type of a given ELF symbol.
Args:
symbol: The ELF symbol object (elf_sym)
symbol_index: The index of the symbol within the type table
Returns:
A single-character string representing the symbol type
"""
if self.has_member("kallsyms") and self.kallsyms.has_member("typetab"):
# kernels >= 5.2 1c7651f43777cdd59c1aaa82c87324d3e7438c7b types have its own array
layer = self._context.layers[self.vol.layer_name]
sym_type = layer.read(self.section_typetab + symbol_index, 1)
sym_type = sym_type.decode("utf-8", errors="ignore")
else:
# kernels < 5.2 the type was stored in the st_info
sym_type = chr(symbol.st_info)
return sym_type
class task_struct(generic.GenericIntelProcess):
def is_valid(self) -> bool:
@@ -371,6 +457,19 @@ class task_struct(generic.GenericIntelProcess):
self._context, dtb, config_prefix, preferred_name
)
def get_address_space_layer(
self,
) -> Optional[interfaces.layers.TranslationLayerInterface]:
"""Returns the task layer for this task's address space."""
task_layer_name = (
self.vol.layer_name if self.is_kernel_thread else self.add_process_layer()
)
if not task_layer_name:
return None
return self._context.layers[task_layer_name]
def get_process_memory_sections(
self, heap_only: bool = False
) -> Generator[Tuple[int, int], None, None]:
@@ -481,6 +580,15 @@ class task_struct(generic.GenericIntelProcess):
else None
)
@property
def state(self):
if self.has_member("__state"):
return self.member("__state")
elif self.has_member("state"):
return self.member("state")
else:
raise AttributeError("Unsupported task_struct: Cannot find state")
def _get_task_start_time(self) -> datetime.timedelta:
"""Returns the task's monotonic start_time as a timedelta.
@@ -951,14 +1059,28 @@ class super_block(objects.StructType):
SB_LAZYTIME: "lazytime",
}
@property
@functools.cached_property
def major(self) -> int:
return self.s_dev >> self.MINORBITS
@property
@functools.cached_property
def minor(self) -> int:
return self.s_dev & ((1 << self.MINORBITS) - 1)
@functools.cached_property
def uuid(self) -> str:
if not self.has_member("s_uuid"):
raise AttributeError(
"super_block struct does not support s_uuid direct attribute access, probably indicating a kernel version < 2.6.39-rc1."
)
if self.s_uuid.has_member("b"):
uuid_as_ints = self.s_uuid.b
else:
uuid_as_ints = self.s_uuid
return str(uuid.UUID(bytes=bytes(uuid_as_ints)))
def get_flags_access(self) -> str:
return "ro" if self.s_flags & self.SB_RDONLY else "rw"
@@ -1057,7 +1179,7 @@ class vm_area_struct(objects.StructType):
parent_layer = self._context.layers[self.vol.layer_name]
return self.vm_pgoff << parent_layer.page_shift
def get_name(self, context, task):
def _do_get_name(self, context, task) -> str:
if self.vm_file != 0:
fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file)
elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk:
@@ -1073,6 +1195,12 @@ class vm_area_struct(objects.StructType):
fname = "Anonymous Mapping"
return fname
def get_name(self, context, task) -> Optional[str]:
try:
return self._do_get_name(context, task)
except exceptions.InvalidAddressException:
return None
# used by malfind
def is_suspicious(self, proclayer=None):
ret = False
@@ -1575,7 +1703,7 @@ class vfsmount(objects.StructType):
'True' if the kernel lacks the 'mount' struct, typically indicating kernel < 3.3.
"""
return not self._context.symbol_space.has_type("mount")
return self.has_member("mnt_parent")
def is_equal(self, vfsmount_ptr) -> bool:
"""Helper to make sure it is comparing two pointers to 'vfsmount'.
@@ -2395,6 +2523,10 @@ class xdp_sock(objects.StructType):
class bpf_prog(objects.StructType):
_BPF_PROG_CHUNK_SHIFT = 6
_BPF_PROG_CHUNK_SIZE = 1 << _BPF_PROG_CHUNK_SHIFT
_BPF_PROG_CHUNK_MASK = ~(_BPF_PROG_CHUNK_SIZE - 1)
def get_type(self) -> Union[str, None]:
"""Returns a string with the eBPF program type"""
@@ -2439,6 +2571,58 @@ class bpf_prog(objects.StructType):
return self.aux.get_name()
def bpf_jit_binary_hdr_address(self) -> int:
"""Return the jitted BPF program start address
Based on bpf_jit_binary_hdr()
Returns:
The BPF program address
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
# In 5.18 (33c9805860e584b194199cab1a1e81f4e6395408) <= kernels < 6.0 (1d5f82d9dd477d5c66e0214a68c3e4f308eadd6d)
# 'bpf_prog_aux' has a 'use_bpf_prog_pack' member
bpf_prog_aux_has_use_bpf_prog_pack = vmlinux.get_type(
"bpf_prog_aux"
).has_member("use_bpf_prog_pack")
if bpf_prog_aux_has_use_bpf_prog_pack and self.aux.use_bpf_prog_pack:
long_mask = (1 << vmlinux_layer.bits_per_register) - 1
addr_mask = self._BPF_PROG_CHUNK_MASK & long_mask
else:
addr_mask = vmlinux_layer.page_mask
real_start = self.bpf_func
return real_start & addr_mask
def get_address_region(self) -> Tuple[int, int]:
"""Returns the start and end memory addresses of the BPF program.
Based on bpf_get_prog_addr_region()
Returns:
A tuple with the addresses representing the memory range (start, end) of the BPF program.
"""
vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
vmlinux_layer = vmlinux.context.layers[vmlinux.layer_name]
# Based on bpf_get_prog_addr_region()
bpf_start_address = self.bpf_jit_binary_hdr_address()
if vmlinux.has_type("bpf_binary_header"):
# kernels >= 3.11 314beb9bcabfd6b4542ccbced2402af2c6f6142a
bpf_binary_header = vmlinux.object(
object_type="bpf_binary_header", offset=bpf_start_address, absolute=True
)
pages = bpf_binary_header.pages
else:
# kernels < 3.11 The first member is always the size
pages = vmlinux.object(
object_type="unsigned int", offset=bpf_start_address, absolute=True
)
bpf_end_address = bpf_start_address + pages * vmlinux_layer.page_size
return bpf_start_address, bpf_end_address
class bpf_prog_aux(objects.StructType):
def get_name(self) -> Union[str, None]:
@@ -3317,3 +3501,136 @@ class scatterlist(objects.StructType):
physical_layer = self._context.layers[physical_layer_name]
for sg in self.for_each_sg():
yield from physical_layer.read(sg.dma_address, sg._sg_dma_len())
class latch_tree_root(objects.StructType):
"""Latched RB-trees implementation"""
@functools.cached_property
def _vmlinux(self):
return linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
@functools.lru_cache
def _get_type_cached(self, name):
return self._vmlinux.get_type(name)
def _get_lt_node_from_rb_node(
self, rb_node, index
) -> Optional[interfaces.objects.ObjectInterface]:
"""Gets the latch tree node from the RBTree node.
Based on __lt_from_rb()
"""
# Unfortunately, we cannot use our LinuxUtilities.container_of() here, since the
# member is indexed by the 'index' variable:
# ltn = container_of(node, struct latch_tree_node, node[idx])
pointer_size = self._get_type_cached("pointer").size
type_dec = self._get_type_cached("latch_tree_node")
member_offset = type_dec.relative_child_offset("node") + index * pointer_size
container_addr = rb_node.vol.offset - member_offset
return self._vmlinux.object(
object_type="latch_tree_node", offset=container_addr, absolute=True
)
def find(
self, key: int, comp_function: Callable
) -> Optional[interfaces.objects.ObjectInterface]:
"""Returns a pointer to the node matching key or None.
Based on latch_tree_find() and __lt_find()
Args:
key (int): Typically an address
comp_function: Callback comparison function to provide the order between the
search key and an element. It's works like the kernel's latch_tree_ops::comp
i.e.: comp_function(key, latch_tree_node)
Returns:
latch_tree_node: A pointer to the node matching key or None.
"""
# latch_tree_root >= 4.2 ade3f510f93a5613b672febe88eff8ea7f1c63b7
# Use the lowest sequence bit as an index for picking which data copy to read
if self.seq.has_member("seqcount"):
# kernels >= 5.10 0c9794c8b6781eb7dad8e19b78c5d4557790597a
sequence = self.seq.seqcount.sequence
elif self.seq.has_member("sequence"):
# 4.2 <= kernel < 5.10
sequence = self.seq.sequence
else:
raise AttributeError("Unsupported sequence type implementation")
idx = sequence & 1
rb_node_ptr = self.tree[idx].rb_node
while rb_node_ptr and rb_node_ptr.is_readable():
rb_node = rb_node_ptr.dereference()
lt_node = self._get_lt_node_from_rb_node(rb_node, idx)
c = comp_function(key, lt_node)
if c < 0:
rb_node_ptr = rb_node.rb_left
elif c > 0:
rb_node_ptr = rb_node.rb_right
else:
return lt_node
return None
class kernel_symbol(objects.StructType):
def _offset_to_ptr(self, off) -> int:
layer = self._context.layers[self.vol.layer_name]
long_mask = (1 << layer.bits_per_register) - 1
return (self.vol.offset + off) & long_mask
def get_name(self) -> str:
if self.has_member("name_offset"):
# kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y
# See 7290d58095712a89f845e1bca05334796dd49ed2
name_offset = self._offset_to_ptr(self.name_offset)
elif self.has_member("name"):
# kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n
name_offset = self.member("name")
else:
raise AttributeError("Unsupported kernel_symbol type implementation")
layer = self._context.layers[self.vol.layer_name]
name_bytes = layer.read(name_offset, linux_constants.KSYM_NAME_LEN)
idx = name_bytes.find(b"\x00")
if idx != -1:
name_bytes = name_bytes[:idx]
return name_bytes.decode("utf-8", errors="ignore")
def get_value(self) -> int:
if self.has_member("value_offset"):
# kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y
# See 7290d58095712a89f845e1bca05334796dd49ed2
return self._offset_to_ptr(self.value_offset)
elif self.has_member("value"):
# kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n
return self.member("value")
raise AttributeError("Unsupported kernel_symbol type implementation")
def get_namespace(self) -> str:
if self.has_member("namespace_offset"):
# kernel >= 4.19 and CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y
# See 7290d58095712a89f845e1bca05334796dd49ed2
namespace_offset = self._offset_to_ptr(self.namespace_offset)
elif self.has_member("namespace"):
# kernel < 4.19 or CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=n
namespace_offset = self.member("namespace")
else:
raise AttributeError("Unsupported kernel_symbol type implementation")
layer = self._context.layers[self.vol.layer_name]
namespace_bytes = layer.read(namespace_offset, linux_constants.KSYM_NAME_LEN)
idx = namespace_bytes.find(b"\x00")
if idx != -1:
namespace_bytes = namespace_bytes[:idx]
return namespace_bytes.decode("utf-8", errors="ignore")
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,66 @@
from typing import Iterator, List, Tuple
import warnings
from typing import Iterable, Iterator, List, Optional, Tuple
from volatility3 import framework
from volatility3.framework import constants, interfaces
from volatility3.framework.objects import utility
from volatility3.framework.symbols.linux import extensions
class Modules(interfaces.configuration.VersionableInterface):
"""Kernel modules related utilities."""
_version = (1, 0, 0)
_version = (1, 1, 0)
_required_framework_version = (2, 0, 0)
framework.require_interface_version(*_required_framework_version)
@classmethod
def module_lookup_by_address(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
modules: Iterable[extensions.module],
target_address: int,
) -> Optional[extensions.module]:
"""
Determine if a target address lies in a module memory space.
Returns the module where the provided address lies.
Args:
context: The context on which to operate
layer_name: The name of the layer on which to operate
modules: An iterable containing the modules to match the address against
target_address: The address to check for a match
Returns:
The first memory module in which the address fits
Kernel documentation:
"within_module" and "within_module_mem_type" functions
"""
matches = []
seen_addresses = set()
for module in modules:
_, start, end = cls.mask_mods_list(context, layer_name, [module])[0]
if (
start <= target_address < end
and module.vol.offset not in seen_addresses
):
matches.append(module)
seen_addresses.add(module.vol.offset)
if len(matches) > 1:
warnings.warn(
f"Address {hex(target_address)} fits in modules at {[hex(module.vol.offset) for module in matches]}, indicating potential modules memory space overlap.",
UserWarning,
)
return matches[0]
elif len(matches) == 1:
return matches[0]
return None
@classmethod
def mask_mods_list(
cls,
@@ -24,6 +24,7 @@ from volatility3.framework.objects import utility
from volatility3.framework.renderers import conversion
from volatility3.framework.symbols import generic
from volatility3.framework.symbols.windows.extensions import pool
from volatility3.framework.symbols import windows
vollog = logging.getLogger(__name__)
@@ -262,14 +263,20 @@ class MMVAD_SHORT(objects.StructType):
def get_commit_charge(self):
"""Get the VAD's commit charge (number of committed pages)"""
if self.has_member("u1") and self.u1.has_member("VadFlags1"):
if self.has_member("CommitCharge"):
return self.CommitCharge
elif self.has_member("u1") and self.u1.has_member("VadFlags1"):
return self.u1.VadFlags1.CommitCharge
elif self.has_member("u") and self.u.has_member("VadFlags"):
return self.u.VadFlags.CommitCharge
elif self.has_member("Core"):
return self.Core.u1.VadFlags1.CommitCharge
if self.Core.has_member("CommitCharge"):
return self.Core.CommitCharge
else:
return self.Core.u1.VadFlags1.CommitCharge
raise AttributeError("Unable to find the commit charge member")
@@ -775,41 +782,130 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
)
return peb
def load_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Generator for DLLs in the order that they were loaded."""
def get_peb32(self) -> Optional[interfaces.objects.ObjectInterface]:
"""Constructs a PEB32 object"""
if constants.BANG not in self.vol.type_name:
raise ValueError(
f"Invalid symbol table name syntax (no {constants.BANG} found)"
)
# add_process_layer can raise InvalidAddressException.
# if that happens, we let the exception propagate upwards
proc_layer_name = self.add_process_layer()
proc_layer = self._context.layers[proc_layer_name]
# Determine if process is running under WOW64.
if self.get_is_wow64():
proc = self.get_wow_64_process()
else:
return None
# Confirm WoW64Process points to a valid process address
if not proc_layer.is_valid(proc):
raise exceptions.InvalidAddressException(
proc_layer_name, proc, f"Invalid Wow64Process address at {self.Peb:0x}"
)
# Leverage the context of existing symbol table to help configure
# a new symbol table for 32-bit types
sym_table = self.get_symbol_table_name()
config_path = self._context.symbol_space[sym_table].config_path
# Load the 32-bit types into a new symbol space
# We use the WindowsKernelIntermedSymbols class to make
# sure we get all the object helpers. For example, traversing
# linked-lists.
self._32bit_table_name = windows.WindowsKernelIntermedSymbols.create(
self._context, config_path, "windows", "wow64"
)
# windows 10
if self._context.symbol_space.has_type(
sym_table + constants.BANG + "_EWOW64PROCESS"
):
offset = proc.Peb
# vista sp0-sp1 and 2003 sp1-sp2
elif self._context.symbol_space.has_type(
sym_table + constants.BANG + "_WOW64_PROCESS"
):
offset = proc.Wow64
else:
offset = proc
peb32 = self._context.object(
f"{self._32bit_table_name}{constants.BANG}_PEB32",
layer_name=proc_layer_name,
offset=offset,
)
return peb32
def set_types(self, peb) -> str:
ldr_data = self._context.symbol_space.get_type(
self._32bit_table_name + constants.BANG + "_PEB_LDR_DATA"
)
peb.Ldr = peb.Ldr.cast("pointer", subtype=ldr_data)
sym_table = self._32bit_table_name
return sym_table
def _walk_ldr_list(
self, list_member: str, link_member: str
) -> Iterable[interfaces.objects.ObjectInterface]:
"""
Walks LDR_DATA_TABLEs and enforces the entries at least have a valid base address
This function also breaks up exception handling as much as possible to ensure the
most data is returned as possible
"""
pebs = []
try:
peb = self.get_peb()
yield from peb.Ldr.InLoadOrderModuleList.to_list(
f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY",
"InLoadOrderLinks",
)
if peb:
pebs.append(peb)
except exceptions.InvalidAddressException:
return None
vollog.debug(f"Process at {self.vol.offset:#x} has invalid PEB")
try:
peb32 = self.get_peb32()
if peb32:
pebs.append(peb32)
except exceptions.InvalidAddressException:
vollog.debug(f"Process at {self.vol.offset:#x} has invalid 32 bit PEB")
for peb in pebs:
sym_table = self.get_symbol_table_name()
if peb.Ldr.vol.type_name.split(constants.BANG)[-1] == ("unsigned long"):
sym_table = self.set_types(peb)
for ldr in peb.Ldr.member(list_member).to_list(
f"{sym_table}{constants.BANG}" + "_LDR_DATA_TABLE_ENTRY", link_member
):
try:
# Several samples in testing crashed from DLLs being returned
# where DllBase was on the next page and that page was not in memory
# Not being able to retrieve the base makes the entry pretty useless
# So we enforce here its presence
ldr.DllBase
yield ldr
except exceptions.InvalidAddressException:
continue
def load_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Generator for DLLs in the order that they were loaded."""
yield from self._walk_ldr_list("InLoadOrderModuleList", "InLoadOrderLinks")
def init_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Generator for DLLs in the order that they were initialized"""
try:
peb = self.get_peb()
yield from peb.Ldr.InInitializationOrderModuleList.to_list(
f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY",
"InInitializationOrderLinks",
)
except exceptions.InvalidAddressException:
return None
yield from self._walk_ldr_list(
"InInitializationOrderModuleList", "InInitializationOrderLinks"
)
def mem_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Generator for DLLs in the order that they appear in memory"""
try:
peb = self.get_peb()
yield from peb.Ldr.InMemoryOrderModuleList.to_list(
f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY",
"InMemoryOrderLinks",
)
except exceptions.InvalidAddressException:
return None
yield from self._walk_ldr_list("InMemoryOrderModuleList", "InMemoryOrderLinks")
def get_handle_count(self):
try:
@@ -832,9 +928,14 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject):
return renderers.NotApplicableValue()
symbol_table_name = self.get_symbol_table_name()
kvo = self._context.layers[self.vol.native_layer_name].config[
"kernel_virtual_offset"
]
kvo = self._context.layers[self.vol.native_layer_name].config.get(
"kernel_virtual_offset", None
)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = self._context.module(
symbol_table_name,
layer_name=self.vol.native_layer_name,
@@ -1024,7 +1125,13 @@ class TOKEN(objects.StructType):
if self.UserAndGroupCount < 0xFFFF:
layer_name = self.vol.layer_name
kvo = self._context.layers[layer_name].config["kernel_virtual_offset"]
kvo = self._context.layers[layer_name].config.get(
"kernel_virtual_offset", None
)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
symbol_table = self.get_symbol_table_name()
ntkrnlmp = self._context.module(
symbol_table, layer_name=layer_name, offset=kvo
@@ -1126,9 +1233,13 @@ class KTIMER(objects.StructType):
def get_dpc(self):
"""Return Dpc, and if Windows 7 or later, decode it"""
symbol_table_name = self.get_symbol_table_name()
kvo = self._context.layers[self.vol.native_layer_name].config[
"kernel_virtual_offset"
]
kvo = self._context.layers[self.vol.native_layer_name].config.get(
"kernel_virtual_offset", None
)
if not kvo:
raise ValueError(
"Intel layer does not have an associated kernel virtual offset, failing"
)
ntkrnlmp = self._context.module(
symbol_table_name,
layer_name=self.vol.native_layer_name,
File diff suppressed because it is too large Load Diff