mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-31 20:29:46 +02:00
Merge branch 'feature/linux_ifconfig_plugin' into linux_ifconfig_plugin
This commit is contained in:
@@ -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,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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
|
||||
@@ -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)]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user