Merge branch 'volatilityfoundation:develop' into linux_sockscan

This commit is contained in:
Eve
2024-03-27 09:25:22 +00:00
committed by GitHub
33 changed files with 1420 additions and 118 deletions
+37
View File
@@ -6,6 +6,7 @@
#
import os
import re
import subprocess
import sys
import shutil
@@ -189,6 +190,16 @@ def test_windows_svcscan(image, volatility, python):
assert rc == 0
def test_windows_thrdscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.thrdscan.ThrdScan", image, volatility, python)
# find pid 4 (of system process) which starts with lowest tids
assert out.find(b"\t4\t8") != -1
assert out.find(b"\t4\t12") != -1
assert out.find(b"\t4\t16") != -1
#assert out.find(b"this raieses AssertionError") != -1
assert rc == 0
def test_windows_privileges(image, volatility, python):
rc, out, err = runvol_plugin(
"windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"]
@@ -331,6 +342,32 @@ def test_linux_tty_check(image, volatility, python):
assert rc == 0
def test_linux_library_list(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.library_list.LibraryList", image, volatility, python
)
assert re.search(
rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert re.search(
rb"gnome-settings-\s3807\s0x7f7e660b5000\s/lib/x86_64-linux-gnu/libbz2.so.1.0",
out,
)
assert re.search(
rb"gdu-notificatio\s3878\s0x7f25ce33e000\s/usr/lib/x86_64-linux-gnu/libXau.so.6",
out,
)
assert re.search(
rb"bash\s8600\s0x7fe78a85f000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert out.count(b"\n") >= 2677
assert rc == 0
# MAC
+31 -4
View File
@@ -22,6 +22,7 @@ import traceback
from typing import Any, Dict, List, Tuple, Type, Union
from urllib import parse, request
from volatility3.cli import text_filter
import volatility3.plugins
import volatility3.symbols
from volatility3 import framework
@@ -233,6 +234,12 @@ class CommandLine:
default=False,
action="store_true",
)
parser.add_argument(
"--filters",
help="List of filters to apply to the output (in the form of [+-]columname,pattern[!])",
default=[],
action="append",
)
parser.set_defaults(**default_config)
@@ -257,12 +264,14 @@ class CommandLine:
file_logger.setFormatter(file_formatter)
rootlog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
if partial_args.verbosity < 1:
sys.tracebacklimit = None
console.setLevel(30 - (partial_args.verbosity * 10))
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
@@ -454,7 +463,10 @@ class CommandLine:
try:
# Construct and run the plugin
if constructed:
renderers[args.renderer]().render(constructed.run())
grid = constructed.run()
renderer = renderers[args.renderer]()
renderer.filter = text_filter.CLIFilter(grid, args.filters)
renderer.render(grid)
except exceptions.VolatilityException as excp:
self.process_exceptions(excp)
@@ -685,6 +697,17 @@ class CommandLine:
)
context.config[extended_path] = value
def order_extra_verbose_levels(self):
for level, level_value in enumerate(
[
constants.LOGLEVEL_V,
constants.LOGLEVEL_VV,
constants.LOGLEVEL_VVV,
constants.LOGLEVEL_VVVV,
]
):
logging.addLevelName(level_value, f"DETAIL {level+1}")
def file_handler_class_factory(self, direct=True):
output_dir = self.output_dir
@@ -817,7 +840,11 @@ class CommandLine:
requirement,
volatility3.framework.configuration.requirements.ListRequirement,
):
additional["type"] = requirement.element_type
# Allow a list of integers, specified with the convenient 0x hexadecimal format
if requirement.element_type == int:
additional["type"] = lambda x: int(x, 0)
else:
additional["type"] = requirement.element_type
nargs = "*" if requirement.optional else "+"
additional["nargs"] = nargs
elif isinstance(
+98
View File
@@ -0,0 +1,98 @@
import logging
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
import re
vollog = logging.getLogger(__name__)
class CLIFilter:
def __init__(self, treegrid, filters: List[str]):
self._filters = self._prepare(treegrid, filters)
def _prepare(self, treegrid: interfaces.renderers.TreeGrid, filters: List[str]):
"""Runs through the filter strings and creates the necessary filter objects"""
output = []
for filter in filters:
exclude = False
regex = False
pattern = None
column_name = None
if filter.startswith("-"):
exclude = True
filter = filter[1:]
elif filter.startswith("+"):
filter = filter[1:]
components = filter.split(",")
if len(components) < 2:
pattern = components[0]
else:
column_name = components[0]
pattern = ",".join(components[1:])
if pattern and pattern.endswith("!"):
regex = True
pattern = pattern[:-1]
column_num = None
if column_name:
for num, column in enumerate(treegrid.columns):
if column_name.lower() in column.name.lower():
column_num = num
break
if pattern:
output.append(ColumnFilter(column_num, pattern, regex, exclude))
vollog.log(constants.LOGLEVEL_VVV, "Filters:\n" + repr(output))
return output
def filter(
self,
row: List[Any],
) -> bool:
"""Filters the row based on each of the column_filters"""
if not self._filters:
return False
found = any([column_filter.found(row) for column_filter in self._filters])
return not found
class ColumnFilter:
def __init__(
self,
column_num: Optional[int],
pattern: str,
regex: bool = False,
exclude: bool = False,
) -> None:
self.column_num = column_num
self.pattern = pattern
self.exclude = exclude
self.regex = regex
def find(self, item) -> bool:
"""Identifies whether an item is found in the appropriate column"""
try:
if self.regex:
return re.search(self.pattern, f"{item}")
return self.pattern in f"{item}"
except IOError:
return False
def found(self, row: List[Any]) -> bool:
"""Determines whether a row should be filtered
If the classes exclude value is false, and the necessary pattern is found, the row is not filtered,
otherwise it is filtered.
"""
if self.column_num is None:
found = any([self.find(x) for x in row])
else:
found = self.find(row[self.column_num])
if self.exclude:
return not found
return found
def __repr__(self) -> str:
"""Returns a display of a column filter"""
return f"ColumnFilter(column={self.column_num},exclude={self.exclude},regex={self.regex},pattern={self.pattern})"
+9
View File
@@ -10,6 +10,7 @@ import string
import sys
from functools import wraps
from typing import Any, Callable, Dict, List, Tuple
from volatility3.cli import text_filter
from volatility3.framework import interfaces, renderers
from volatility3.framework.renderers import format_hints
@@ -134,6 +135,7 @@ class CLIRenderer(interfaces.renderers.Renderer):
name = "unnamed"
structured_output = False
filter: text_filter.CLIFilter = None
class QuickTextRenderer(CLIRenderer):
@@ -172,6 +174,9 @@ class QuickTextRenderer(CLIRenderer):
outfd.write("\n{}\n".format("\t".join(line)))
def visitor(node: interfaces.renderers.TreeNode, accumulator):
if self.filter and self.filter.filter(node.values):
return accumulator
accumulator.write("\n")
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
accumulator.write(
@@ -306,6 +311,10 @@ class PrettyTextRenderer(CLIRenderer):
max_column_widths[tree_indent_column] = max(
max_column_widths.get(tree_indent_column, 0), node.path_depth
)
if self.filter and self.filter.filter(node.values):
return accumulator
line = {}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
+3 -2
View File
@@ -197,10 +197,11 @@ class VolShell(cli.CommandLine):
vollog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
console.setLevel(30 - (partial_args.verbosity * 10))
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
+9 -5
View File
@@ -44,7 +44,7 @@ BANG = "!"
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 6 # Number of changes that only add to the interface
VERSION_MINOR = 7 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
@@ -59,14 +59,18 @@ PACKAGE_VERSION = (
AUTOMAGIC_CONFIG_PATH = "automagic"
"""The root section within the context configuration for automagic values"""
LOGLEVEL_INFO = 20
"""Logging level for information data, showed when use the requests any logging: -v"""
LOGLEVEL_DEBUG = 10
"""Logging level for debugging data, showed when the user requests more logging detail: -vv"""
LOGLEVEL_V = 9
"""Logging level for a single -v"""
"""Logging level for the lowest "extra" level of logging: -vvv"""
LOGLEVEL_VV = 8
"""Logging level for -vv"""
"""Logging level for two levels of detail: -vvvv"""
LOGLEVEL_VVV = 7
"""Logging level for -vvv"""
"""Logging level for three levels of detail: -vvvvv"""
LOGLEVEL_VVVV = 6
"""Logging level for -vvvv"""
"""Logging level for four levels of detail: -vvvvvv"""
CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3")
"""Default path to store cached data"""
@@ -5,11 +5,10 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import IntEnum
KERNEL_NAME = "__kernel__"
# arch/x86/include/asm/page_types.h
PAGE_SHIFT = 12
"""The value hard coded from the Linux Kernel (hence not extracted from the layer itself)"""
# include/linux/sched.h
@@ -281,3 +280,25 @@ CAPABILITIES = (
)
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1
class ELF_IDENT(IntEnum):
"""ELF header e_ident indexes"""
EI_MAG0 = 0
EI_MAG1 = 1
EI_MAG2 = 2
EI_MAG3 = 3
EI_CLASS = 4
EI_DATA = 5
EI_VERSION = 6
EI_OSABI = 7
EI_PAD = 8
class ELF_CLASS(IntEnum):
"""ELF header class types"""
ELFCLASSNONE = 0
ELFCLASS32 = 1
ELFCLASS64 = 2
+1 -1
View File
@@ -60,7 +60,7 @@ class FileHandlerInterface(io.RawIOBase):
@staticmethod
def sanitize_filename(filename: str) -> str:
"""Sanititizes the filename to ensure only a specific whitelist of characters is allowed through"""
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]\{\}!$%^:#~?<>,|"
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]{}!$%^:#~?<>,|"
result = ""
for char in filename:
if char in allowed:
+5 -1
View File
@@ -261,11 +261,15 @@ class WindowsCrashDumpStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
for layer in [WindowsCrashDump32Layer, WindowsCrashDump64Layer]:
with contextlib.suppress(WindowsCrashDumpFormatException):
try:
layer.check_header(context.layers[layer_name])
new_name = context.layers.free_layer_name(layer.__name__)
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return layer(context, new_name, new_name)
except WindowsCrashDumpFormatException as excp:
vollog.log(
constants.LOGLEVEL_VVVV, f"Exception reading crashdump: {excp}"
)
return None
+13 -2
View File
@@ -6,9 +6,11 @@ import struct
from typing import Optional
from volatility3.framework import exceptions, interfaces, constants
from volatility3.framework.constants.linux import ELF_CLASS
from volatility3.framework.layers import segmented
from volatility3.framework.symbols import intermed
vollog = logging.getLogger(__name__)
@@ -21,7 +23,7 @@ class Elf64Layer(segmented.SegmentedLayer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -50,8 +52,17 @@ class Elf64Layer(segmented.SegmentedLayer):
offset=ehdr.e_phoff + (pindex * ehdr.e_phentsize),
)
# We only want PT_TYPES with valid sizes
try:
ptype = phdr.p_type.description
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
if (
phdr.p_type.lookup() == "PT_LOAD"
ptype == "PT_LOAD"
and phdr.p_filesz == phdr.p_memsz
and phdr.p_filesz > 0
):
+12
View File
@@ -67,6 +67,12 @@ class Intel(linear.LinearlyMappedLayer):
math.ceil(math.log2(struct.calcsize(self._entry_format)))
)
@classproperty
@functools.lru_cache()
def page_shift(cls) -> int:
"""Page shift for the intel memory layers."""
return cls._page_size_in_bits
@classproperty
@functools.lru_cache()
def page_size(cls) -> int:
@@ -76,6 +82,12 @@ class Intel(linear.LinearlyMappedLayer):
"""
return 1 << cls._page_size_in_bits
@classproperty
@functools.lru_cache()
def page_mask(cls) -> int:
"""Page mask for the intel memory layers."""
return ~(cls.page_size - 1)
@classproperty
@functools.lru_cache()
def bits_per_register(cls) -> int:
+4 -5
View File
@@ -5,6 +5,7 @@ from typing import Optional
from volatility3.framework import constants, interfaces, exceptions
from volatility3.framework.layers import elf
from volatility3.framework.symbols import intermed
from volatility3.framework.constants.linux import ELF_CLASS
vollog = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -115,12 +116,10 @@ class XenCoreDumpLayer(elf.Elf64Layer):
)
)
elif p2m_data and pfn_data:
raise elf.ElfFormatException(
self.name, f"Both P2M and PFN in Xen Core Dump"
)
raise elf.ElfFormatException(self.name, "Both P2M and PFN in Xen Core Dump")
else:
raise elf.ElfFormatException(
self.name, f"Neither P2M nor PFN in Xen Core Dump"
self.name, "Neither P2M nor PFN in Xen Core Dump"
)
if len(segments) == 0:
+19 -15
View File
@@ -14,8 +14,10 @@ from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.framework.constants.linux import ELF_MAX_EXTRACTION_SIZE
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
@@ -23,7 +25,7 @@ class Elfs(plugins.PluginInterface):
"""Lists all memory mapped ELF files for all processes."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
_version = (2, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -87,7 +89,14 @@ class Elfs(plugins.PluginInterface):
sections = {}
# TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ?
for phdr in elf_object.get_program_headers():
if phdr.p_type != 1: # PT_LOAD = 1
try:
if phdr.p_type.description != "PT_LOAD":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
start = phdr.p_vaddr
@@ -95,18 +104,18 @@ class Elfs(plugins.PluginInterface):
end = start + size
# Use complete memory pages for dumping
# If start isn't a multiple of 4096, stick to the highest multiple < start
# If end isn't a multiple of 4096, stick to the lowest multiple > end
if start % 4096:
start = start & ~0xFFF
# If start isn't a multiple of a page, stick to the highest multiple < start
# If end isn't a multiple of a page, stick to the lowest multiple > end
if start % proc_layer.page_size:
start = start & proc_layer.page_mask
if end % 4096:
end = (end & ~0xFFF) + 4096
if end % proc_layer.page_size:
end = (end & proc_layer.page_mask) + proc_layer.page_size
real_size = end - start
# Check if ELF has a legitimate size
if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE:
if real_size < 0 or real_size > ELF_MAX_EXTRACTION_SIZE:
raise ValueError(f"The claimed size of the ELF is invalid: {real_size}")
sections[start] = real_size
@@ -140,12 +149,7 @@ class Elfs(plugins.PluginInterface):
for vma in task.mm.get_vma_iter():
hdr = proc_layer.read(vma.vm_start, 4, pad=True)
if not (
hdr[0] == 0x7F
and hdr[1] == 0x45
and hdr[2] == 0x4C
and hdr[3] == 0x46
):
if hdr != b"\x7fELF":
continue
path = vma.get_name(self.context, task)
+3 -1
View File
@@ -198,7 +198,9 @@ class ABCKmsg(ABC):
class Kmsg_pre_3_5(ABCKmsg):
"""The kernel ring buffer (log_buf) is a char array that sequentially stores
log lines, each separated by newline (LF) characters. i.e:
<6>[ 9565.250411] line1!\n<6>[ 9565.250412] line2\n...
<6>[ 9565.250411] line1!\\n<6>[ 9565.250412] line2\\n...
"""
@classmethod
@@ -0,0 +1,169 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import Iterable, Tuple
from volatility3.framework import interfaces, renderers, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class LibraryList(interfaces.plugins.PluginInterface):
"""Enumerate libraries loaded into processes"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 2, 0)
),
requirements.ListRequirement(
name="pids",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _get_libdl_libraries(
self, proc_layer_name: str, vma_start: int
) -> interfaces.objects.ObjectInterface:
"""Get the ELF link map objects for the given VMA address
Args:
proc_layer_name (str): Name of the process layer
vma_start (int): VMA start address
Yields:
ELF link map objects for the given VMA address
"""
elf_table_name = intermed.IntermediateSymbolTable.create(
self.context,
self.config_path,
"linux",
"elf",
class_types=elf.class_types,
)
elf_object = self.context.object(
elf_table_name + constants.BANG + "Elf",
offset=vma_start,
layer_name=proc_layer_name,
)
if not elf_object or not elf_object.is_valid():
return None
kernel = self.context.modules[self.config["kernel"]]
try:
for link_map in elf_object.get_link_maps(kernel.symbol_table_name):
if link_map.l_addr and link_map.l_name:
yield link_map
except exceptions.InvalidAddressException:
# Protection against memory smear in this VMA
pass
def _get_libdl_maps(
self, task: interfaces.objects.ObjectInterface, proc_layer_name: str
) -> interfaces.objects.ObjectInterface:
"""Get the ELF link maps objects for a task
Args:
task (task_struct): A reference task
proc_layer_name (str): Name of the process layer
Yields:
ELF link map objects
"""
link_map_seen = set()
for vma in task.mm.get_vma_iter():
for link_map in self._get_libdl_libraries(proc_layer_name, vma.vm_start):
if link_map.l_addr in link_map_seen:
continue
yield link_map
link_map_seen.add(link_map.l_addr)
def _get_task_libraries(
self, task: interfaces.objects.ObjectInterface
) -> Tuple[int, str]:
"""Get the task libraries from the ELF headers found within the memory maps
Args:
task (task_struct): The reference task
Yields:
Tuples with a ELF link map address and name
"""
proc_layer_name = task.add_process_layer()
if not proc_layer_name:
return
for elf_link_map in self._get_libdl_maps(task, proc_layer_name):
name = elf_link_map.get_name()
if not name:
continue
yield elf_link_map.l_addr, name
def _get_tasks_libraries(
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
) -> Iterable[Tuple[str, int, int, str]]:
"""Get the task libraries from the ELF headers found within the memory maps for
all the tasks.
Args:
tasks: An iterable of tasks
Yields:
Tuples with a task name, task tgid, an ELF link map address and name
"""
for task in tasks:
task_name = utility.array_to_string(task.comm)
for linkmap_addr, linkmap_name in self._get_task_libraries(task):
yield task_name, task.tgid, linkmap_addr, linkmap_name
def _format_fields(self, fields):
task_name, task_pid, addr, name = fields
return task_name, task_pid, format_hints.Hex(addr), name
def _generator(
self, tasks: Iterable[interfaces.objects.ObjectInterface]
) -> Iterable[Tuple[int, Tuple]]:
for fields in self._get_tasks_libraries(tasks):
yield 0, self._format_fields(fields)
def run(self):
pids = self.config.get("pids")
pid_filter = pslist.PsList.create_pid_filter(pids)
tasks = pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=pid_filter
)
headers = [
("Name", str),
("Pid", int),
("LoadAddress", format_hints.Hex),
("Path", str),
]
return renderers.TreeGrid(headers, self._generator(tasks))
@@ -83,11 +83,11 @@ class PsList(interfaces.plugins.PluginInterface):
cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, str]:
"""Extract the fields needed for the final output
Args:
task: A task object from where to get the fields.
decorate_comm: If True, it decorates the comm string of
- User threads: in curly brackets,
- Kernel threads: in square brackets
decorate_comm: If True, it decorates the comm string of user threads in curly brackets,
and of Kernel threads in square brackets.
Defaults to False.
Returns:
A tuple with the fields to show in the plugin output.
@@ -28,7 +28,7 @@ class PsScan(interfaces.plugins.PluginInterface):
"""Scans for processes present in a particular linux image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -139,7 +139,7 @@ class PsScan(interfaces.plugins.PluginInterface):
kernel_layer_name, f"Layer {kernel_layer_name} has no dependencies"
)
memory_layer_name = kernel_layer.dependencies[0]
memory_layer = context.layers[kernel_layer.dependencies[0]]
memory_layer = context.layers[memory_layer_name]
# scan the memory_layer for these needles
for address, _ in memory_layer.scan(
@@ -83,7 +83,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
sock: Kernel generic `sock` object
Returns a tuple with:
sock: The respective kernel's \*_sock object for that socket family
sock: The respective kernel's \\*_sock object for that socket family
sock_stat: A tuple with the source and destination (address and port) along with its state string
socket_filter: A dictionary with information about the socket filter
"""
@@ -501,7 +501,7 @@ class Sockstat(plugins.PluginInterface):
family: Socket family string (AF_UNIX, AF_INET, etc)
sock_type: Socket type string (STREAM, DGRAM, etc)
protocol: Protocol string (UDP, TCP, etc)
sock_fields: A tuple with the \*_sock object, the sock stats and the extended info dictionary
sock_fields: A tuple with the \\*_sock object, the sock stats and the extended info dictionary
"""
vmlinux = context.modules[symbol_table]
@@ -0,0 +1,79 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from volatility3.framework import interfaces, renderers, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
vollog = logging.getLogger(__name__)
class Dmesg(interfaces.plugins.PluginInterface):
"""Prints the kernel log buffer."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def get_kernel_log_buffer(
cls, context: interfaces.context.ContextInterface, kernel_module_name: str
):
"""
Online documentation :
- https://github.com/apple-open-source/macos/blob/master/xnu/bsd/sys/msgbuf.h
- https://github.com/apple-open-source/macos/blob/ea4cd5a06831aca49e33df829d2976d6de5316ec/xnu/bsd/kern/subr_log.c#L751
Volatility 2 plugin :
- https://github.com/volatilityfoundation/volatility/blob/master/volatility/plugins/mac/dmesg.py
"""
kernel = context.modules[kernel_module_name]
if not kernel.has_symbol("msgbufp"):
raise exceptions.SymbolError(
"msgbufp",
kernel.symbol_table_name,
'The provided symbol table does not include the "msgbufp" symbol. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.',
)
msgbufp = kernel.object_from_symbol(symbol_name="msgbufp")
msg_size = msgbufp.msg_size # max buffer size
msg_bufx = msgbufp.msg_bufx # write index of the msg_bufc circular buffer
msg_bufc = msgbufp.msg_bufc
# msg_bufc is circular, meaning that if its size exceeds msg_size,
# msg_bufx will point to the beginning of the buffer and start overwriting.
msg_bufc_data: str = utility.pointer_to_string(msg_bufc, msg_size)
# Avoid OOB reads
msg_bufx = msg_bufx if msg_bufx <= msg_size else 0
# We directly take into account the case where the write buffer did a loop,
# as older messages will start at msg_bufx offset (not overwritten yet).
dmesg = msg_bufc_data[msg_bufx:]
dmesg += msg_bufc_data[:msg_bufx]
# Yield each line
for dmesg_line in dmesg.splitlines():
yield (dmesg_line,)
def _generator(self):
for value in self.get_kernel_log_buffer(
context=self.context, kernel_module_name=self.config["kernel"]
):
yield (0, value)
def run(self):
return renderers.TreeGrid(
[
("line", str),
],
self._generator(),
)
+166 -4
View File
@@ -2,17 +2,23 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from volatility3.framework import renderers, interfaces
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.mac import pslist
from typing import Callable, Generator, Type, Optional
import logging
vollog = logging.getLogger(__name__)
class Maps(interfaces.plugins.PluginInterface):
"""Lists process memory ranges that potentially contain injected code."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
@classmethod
def get_requirements(cls):
@@ -31,14 +37,152 @@ class Maps(interfaces.plugins.PluginInterface):
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed memory segments",
default=False,
optional=True,
),
requirements.ListRequirement(
name="address",
description="Process virtual memory addresses to include "
"(all other VMA sections are excluded). This can be any "
"virtual address within the VMA section. Virtual addresses "
"must be separated by a space.",
element_type=int,
optional=True,
),
requirements.IntRequirement(
name="maxsize",
description="Maximum size for dumped VMA sections "
"(all the bigger sections will be ignored)",
default=cls.MAXSIZE_DEFAULT,
optional=True,
),
]
@classmethod
def list_vmas(
cls,
task: interfaces.objects.ObjectInterface,
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: True,
) -> Generator[interfaces.objects.ObjectInterface, None, None]:
"""Lists the Virtual Memory Areas of a specific process.
Args:
task: task object from which to list the vma
filter_func: Function to take a vma and return False if it should be filtered out
Returns:
Yields vmas based on the task and filtered based on the filter function
"""
for vma in task.get_map_iter():
if filter_func(vma):
yield vma
else:
vollog.debug(
f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.p_pid} due to filter_func"
)
@classmethod
def vma_dump(
cls,
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
vm_start: int,
vm_end: int,
open_method: Type[interfaces.plugins.FileHandlerInterface],
maxsize: int = MAXSIZE_DEFAULT,
) -> Optional[interfaces.plugins.FileHandlerInterface]:
"""Extracts the complete data for VMA as a FileInterface.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
task: an task_struct instance
vm_start: The start virtual address from the vma to dump
vm_end: The end virtual address from the vma to dump
open_method: class to provide context manager for opening the file
maxsize: Max size of VMA section (default MAXSIZE_DEFAULT)
Returns:
An open FileInterface object containing the complete data for the task or None in the case of failure
"""
pid = task.p_pid
try:
proc_layer_name = task.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
pid, excp.invalid_address, excp.layer_name
)
)
return None
vm_size = vm_end - vm_start
# check if vm_size is negative, this should never happen.
if vm_size < 0:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative."
)
return None
# check if vm_size is larger than the maxsize limit, and therefore is not saved out.
if maxsize <= vm_size:
vollog.warning(
f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}"
)
return None
proc_layer = context.layers[proc_layer_name]
file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp"
try:
file_handle = open_method(file_name)
chunk_size = 1024 * 1024 * 10
offset = vm_start
while offset < vm_start + vm_size:
to_read = min(chunk_size, vm_start + vm_size - offset)
data = proc_layer.read(offset, to_read, pad=True)
file_handle.write(data)
offset += to_read
except Exception as excp:
vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}")
return None
return file_handle
def _generator(self, tasks):
address_list = self.config.get("address", None)
if not address_list:
# do not filter as no address_list was supplied
vma_filter_func = lambda _: True
else:
# filter for any vm_start that matches the supplied address config
def vma_filter_function(task: interfaces.objects.ObjectInterface) -> bool:
addrs_in_vma = [
addr
for addr in address_list
if task.links.start <= addr <= task.links.end
]
# if any of the user supplied addresses would fall within this vma return true
return bool(addrs_in_vma)
vma_filter_func = vma_filter_function
for task in tasks:
process_name = utility.array_to_string(task.p_comm)
process_pid = task.p_pid
for vma in task.get_map_iter():
for vma in self.list_vmas(task, filter_func=vma_filter_func):
try:
vm_start = vma.links.start
vm_end = vma.links.end
except AttributeError:
vollog.debug(
f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {process_pid}"
)
continue
path = vma.get_path(
self.context,
self.context.modules[self.config["kernel"]].symbol_table_name,
@@ -46,15 +190,32 @@ class Maps(interfaces.plugins.PluginInterface):
if path == "":
path = vma.get_special_path()
file_output = "Disabled"
if self.config["dump"]:
file_output = "Error outputting file"
file_handle = self.vma_dump(
self.context,
task,
vm_start,
vm_end,
self.open,
self.config["maxsize"],
)
if file_handle:
file_handle.close()
file_output = file_handle.preferred_filename
yield (
0,
(
process_pid,
process_name,
format_hints.Hex(vma.links.start),
format_hints.Hex(vma.links.end),
format_hints.Hex(vm_start),
format_hints.Hex(vm_end),
vma.get_perms(),
path,
file_output,
),
)
@@ -72,6 +233,7 @@ class Maps(interfaces.plugins.PluginInterface):
("End", format_hints.Hex),
("Protection", str),
("Map Name", str),
("File output", str),
],
self._generator(
list_tasks(self.context, self.config["kernel"], filter_func=filter_func)
@@ -13,7 +13,7 @@ from volatility3.framework.renderers import conversion, format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.plugins import timeliner
from volatility3.plugins.windows import info, pslist
from volatility3.plugins.windows import info, pslist, psscan
vollog = logging.getLogger(__name__)
@@ -36,6 +36,9 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="psscan", component=psscan.PsScan, version=(1, 1, 0)
),
requirements.VersionRequirement(
name="info", component=info.Info, version=(1, 0, 0)
),
@@ -45,6 +48,11 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
description="Process IDs to include (all other processes are excluded)",
optional=True,
),
requirements.IntRequirement(
name="offset",
description="Process offset in the physical address space",
optional=True,
),
requirements.BooleanRequirement(
name="dump",
description="Extract listed DLLs",
@@ -221,6 +229,25 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
kernel = self.context.modules[self.config["kernel"]]
if self.config["offset"]:
procs = psscan.PsScan.scan_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func=psscan.PsScan.create_offset_filter(
self.context,
kernel.layer_name,
self.config["offset"],
),
)
else:
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
return renderers.TreeGrid(
[
("PID", int),
@@ -232,12 +259,5 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
("LoadTime", datetime.datetime),
("File output", str),
],
self._generator(
pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
),
self._generator(procs=procs),
)
@@ -81,7 +81,10 @@ class DriverIrp(interfaces.plugins.PluginInterface):
address
)
module_found = False
for module_name, symbol_generator in module_symbols:
module_found = True
symbols_found = False
for symbol in symbol_generator:
@@ -111,6 +114,19 @@ class DriverIrp(interfaces.plugins.PluginInterface):
),
)
if not module_found:
yield (
0,
(
format_hints.Hex(driver.vol.offset),
driver_name,
MAJOR_FUNCTIONS[i],
format_hints.Hex(address),
renderers.NotAvailableValue(),
renderers.NotAvailableValue(),
),
)
def run(self):
return renderers.TreeGrid(
[
@@ -9,7 +9,7 @@ from volatility3.framework import constants, exceptions, renderers, interfaces,
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import pslist
from volatility3.plugins.windows import pslist, psscan
vollog = logging.getLogger(__name__)
@@ -43,14 +43,22 @@ class Handles(interfaces.plugins.PluginInterface):
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="psscan", component=psscan.PsScan, version=(1, 1, 0)
),
requirements.ListRequirement(
name="pid",
element_type=int,
description="Process IDs to include (all other processes are excluded)",
optional=True,
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
requirements.IntRequirement(
name="offset",
description="Process offset in the physical address space",
optional=True,
),
]
@@ -416,6 +424,25 @@ class Handles(interfaces.plugins.PluginInterface):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
kernel = self.context.modules[self.config["kernel"]]
if self.config["offset"]:
procs = psscan.PsScan.scan_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func=psscan.PsScan.create_offset_filter(
self.context,
kernel.layer_name,
self.config["offset"],
),
)
else:
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
filter_func=filter_func,
)
return renderers.TreeGrid(
[
("PID", int),
@@ -426,12 +453,5 @@ class Handles(interfaces.plugins.PluginInterface):
("GrantedAccess", format_hints.Hex),
("Name", str),
],
self._generator(
pslist.PsList.list_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func=filter_func,
)
),
self._generator(procs=procs),
)
@@ -38,7 +38,7 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -197,7 +197,7 @@ class ADS(interfaces.plugins.PluginInterface):
# Yara Rule to scan for MFT Header Signatures
rules = yarascan.YaraScan.process_yara_options(
{"yara_rules": "/FILE0|FILE\*|BAAD/"}
{"yara_rules": "/FILE0|FILE\\*|BAAD/"}
)
# Read in the Symbol File
@@ -222,6 +222,24 @@ class PoolScanner(plugins.PluginInterface):
type_name=symbol_table + constants.BANG + "_EPROCESS",
object_type="Process",
size=(600, None),
skip_type_test=True,
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# threads on windows before windows8
PoolConstraint(
b"Thr\xe5", # -> “protected” allocation, MSB is set.
type_name=symbol_table + constants.BANG + "_ETHREAD",
object_type="Thread",
size=(600, None), # -> 0x0258 - size of struct in win5.1
skip_type_test=True,
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# threads on windows starting with windows8
PoolConstraint(
b"Thre",
type_name=symbol_table + constants.BANG + "_ETHREAD",
object_type="Thread",
size=(600, None), # -> 0x0258 - size of struct in win5.1
page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE,
),
# files on windows before windows 8
@@ -59,6 +59,75 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
]
@classmethod
def physical_offset_from_virtual(cls, context, layer_name, proc):
"""Calculate the physical offset from the virtual offset of a process.
Args:
context: The context containing layers and modules information.
layer_name: The name of the layer containing the process memory.
proc: The process object for which to calculate the physical offset.
Returns:
int: The physical offset of the process.
Raises:
TypeError: If the primary layer is not an Intel layer.
"""
memory = context.layers[layer_name]
if not isinstance(memory, layers.intel.Intel):
raise TypeError("Primary layer is not an intel layer")
(_, _, ph_offset, _, _) = list(
memory.mapping(offset=proc.vol.offset, length=0)
)[0]
return ph_offset
@classmethod
def create_offset_filter(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
offset: int = None,
physical: bool = True,
exclude: bool = False,
) -> Callable[[interfaces.objects.ObjectInterface], bool]:
"""A factory for producing filter functions that filter based on the physical offset of the process.
Args:
offset: A number that is the physical offset to be filtered out
exclude: Accept only tasks that are not the offset argument
Returns:
Filter function to be passed to the list of processes.
"""
filter_func = lambda _: False
if offset:
if physical:
if exclude:
filter_func = (
lambda proc: cls.physical_offset_from_virtual(
context, layer_name, proc
)
== offset
)
else:
filter_func = (
lambda proc: cls.physical_offset_from_virtual(
context, layer_name, proc
)
!= offset
)
else:
if exclude:
filter_func = lambda proc: proc.vol.offset == offset
else:
filter_func = lambda proc: proc.vol.offset != offset
return filter_func
@classmethod
def scan_processes(
cls,
@@ -0,0 +1,141 @@
##
## plugin for testing addition of threads scan support to poolscanner.py
##
import logging
import datetime
from typing import Iterable
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
from volatility3.plugins.windows import poolscanner
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Scans for windows threads."""
# version 2.6.0 adds support for scanning for 'Ethread' structures by pool tags
_required_framework_version = (2, 6, 0)
_version = (1, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0)
),
]
@classmethod
def scan_threads(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
symbol_table: str,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Scans for threads using the poolscanner module and constraints.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
layer_name: The name of the layer on which to operate
symbol_table: The name of the table containing the kernel symbols
Returns:
A list of _ETHREAD objects found by scanning memory for the "Thre" / "Thr\\xE5" pool signatures
"""
constraints = poolscanner.PoolScanner.builtin_constraints(
symbol_table, [b"Thr\xe5", b"Thre"]
)
for result in poolscanner.PoolScanner.generate_pool_scan(
context, layer_name, symbol_table, constraints
):
_constraint, mem_object, _header = result
yield mem_object
def _generator(self):
kernel = self.context.modules[self.config["kernel"]]
for ethread in self.scan_threads(
self.context, kernel.layer_name, kernel.symbol_table_name
):
try:
thread_offset = ethread.vol.offset
owner_proc_pid = ethread.Cid.UniqueProcess
thread_tid = ethread.Cid.UniqueThread
thread_start_addr = ethread.StartAddress
thread_create_time = (
ethread.get_create_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
thread_exit_time = (
ethread.get_exit_time()
) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object
except (ValueError, exceptions.InvalidAddressException):
vollog.debug(
"Thread :{}, invalid address {} in layer {}".format(
thread_tid, thread_start_addr, kernel.layer_name
)
)
continue
yield (
0,
(
format_hints.Hex(thread_offset),
owner_proc_pid,
thread_tid,
format_hints.Hex(thread_start_addr),
thread_create_time,
thread_exit_time,
),
)
def generate_timeline(self):
for row in self._generator():
_depth, row_data = row
row_dict = {}
(
row_dict["Offset"],
row_dict["PID"],
row_dict["TID"],
row_dict["StartAddress"],
row_dict["CreateTime"],
row_dict["ExitTime"],
) = row_data
# Skip threads with no creation time
# - mainly system process threads
if not isinstance(row_dict["CreateTime"], datetime.datetime):
continue
description = f"Thread: Tid {row_dict['TID']} in Pid {row_dict['PID']} (Offset {row_dict['Offset']})"
# yield created time, and if there is exit time, yield it too.
yield (description, timeliner.TimeLinerType.CREATED, row_dict["CreateTime"])
if isinstance(row_dict["ExitTime"], datetime.datetime):
yield (
description,
timeliner.TimeLinerType.MODIFIED,
row_dict["ExitTime"],
)
def run(self):
return renderers.TreeGrid(
[
("Offset", format_hints.Hex),
("PID", int),
("TID", int),
("StartAddress", format_hints.Hex),
("CreateTime", datetime.datetime),
("ExitTime", datetime.datetime),
],
self._generator(),
)
+135 -7
View File
@@ -270,8 +270,8 @@
"d_tag": {
"offset": 0,
"type": {
"kind": "base",
"name": "long long"
"kind": "enum",
"name": "DtypeEnum64"
}
},
"d_ptr": {
@@ -699,8 +699,8 @@
"d_tag": {
"offset": 0,
"type": {
"kind": "base",
"name": "long"
"kind": "enum",
"name": "DtypeEnum32"
}
},
"d_ptr": {
@@ -905,11 +905,139 @@
"PT_PHDR": 6,
"PT_TLS": 7,
"PT_LOOS": 1610612736,
"PT_GNU_EH_FRAME": 1685382480,
"PT_GNU_STACK": 1685382481,
"PT_GNU_RELRO": 1685382482,
"PT_GNU_PROPERTY": 1685382483,
"PT_HIOS": 1879048191,
"PT_LOWPROC": 1879048192,
"PT_HIPROC": 2147483647
},
"size": 4
},
"DtypeEnum32": {
"base": "long",
"constants": {
"DT_NULL": 0,
"DT_NEEDED": 1,
"DT_PLTRELSZ": 2,
"DT_PLTGOT": 3,
"DT_HASH": 4,
"DT_STRTAB": 5,
"DT_SYMTAB": 6,
"DT_RELA": 7,
"DT_RELASZ": 8,
"DT_RELAENT": 9,
"DT_STRSZ": 10,
"DT_SYMENT": 11,
"DT_INIT": 12,
"DT_FINI": 13,
"DT_SONAME": 14,
"DT_RPATH": 15,
"DT_SYMBOLIC": 16,
"DT_REL": 17,
"DT_RELSZ": 18,
"DT_RELENT": 19,
"DT_PLTREL": 20,
"DT_DEBUG": 21,
"DT_TEXTREL": 22,
"DT_JMPREL": 23,
"DT_BIND_NOW": 24,
"DT_INIT_ARRAY": 25,
"DT_FINI_ARRAY": 26,
"DT_INIT_ARRAYSZ": 27,
"DT_FINI_ARRAYSZ": 28,
"DT_RUNPATH": 29,
"DT_FLAGS": 30,
"DT_ENCODING": 32,
"DT_PREINIT_ARRAYSZ": 33,
"DT_SYMTAB_SHNDX": 34,
"DT_RELRSZ": 35,
"DT_RELR": 36,
"DT_RELRENT": 37,
"DT_NUM": 38,
"OLD_DT_LOOS": 1610612736,
"DT_LOOS": 1610612749,
"DT_HIOS": 1879044096,
"DT_VALRNGLO": 1879047424,
"DT_VALRNGHI": 1879047679,
"DT_ADDRRNGLO": 1879047680,
"DT_GNU_HASH": 1879047925,
"DT_ADDRRNGHI": 1879047935,
"DT_VERSYM": 1879048176,
"DT_RELACOUNT": 1879048185,
"DT_RELCOUNT": 1879048186,
"DT_FLAGS_1": 1879048187,
"DT_VERDEF": 1879048188,
"DT_VERDEFNUM": 1879048189,
"DT_VERNEED": 1879048190,
"DT_VERNEEDNUM": 1879048191,
"DT_LOPROC": 1879048192,
"DT_HIPROC": 2147483647
},
"size": 4
},
"DtypeEnum64": {
"base": "long long",
"constants": {
"DT_NULL": 0,
"DT_NEEDED": 1,
"DT_PLTRELSZ": 2,
"DT_PLTGOT": 3,
"DT_HASH": 4,
"DT_STRTAB": 5,
"DT_SYMTAB": 6,
"DT_RELA": 7,
"DT_RELASZ": 8,
"DT_RELAENT": 9,
"DT_STRSZ": 10,
"DT_SYMENT": 11,
"DT_INIT": 12,
"DT_FINI": 13,
"DT_SONAME": 14,
"DT_RPATH": 15,
"DT_SYMBOLIC": 16,
"DT_REL": 17,
"DT_RELSZ": 18,
"DT_RELENT": 19,
"DT_PLTREL": 20,
"DT_DEBUG": 21,
"DT_TEXTREL": 22,
"DT_JMPREL": 23,
"DT_BIND_NOW": 24,
"DT_INIT_ARRAY": 25,
"DT_FINI_ARRAY": 26,
"DT_INIT_ARRAYSZ": 27,
"DT_FINI_ARRAYSZ": 28,
"DT_RUNPATH": 29,
"DT_FLAGS": 30,
"DT_ENCODING": 32,
"DT_PREINIT_ARRAYSZ": 33,
"DT_SYMTAB_SHNDX": 34,
"DT_RELRSZ": 35,
"DT_RELR": 36,
"DT_RELRENT": 37,
"DT_NUM": 38,
"OLD_DT_LOOS": 1610612736,
"DT_LOOS": 1610612749,
"DT_HIOS": 1879044096,
"DT_VALRNGLO": 1879047424,
"DT_VALRNGHI": 1879047679,
"DT_ADDRRNGLO": 1879047680,
"DT_GNU_HASH": 1879047925,
"DT_ADDRRNGHI": 1879047935,
"DT_VERSYM": 1879048176,
"DT_RELACOUNT": 1879048185,
"DT_RELCOUNT": 1879048186,
"DT_FLAGS_1": 1879048187,
"DT_VERDEF": 1879048188,
"DT_VERDEFNUM": 1879048189,
"DT_VERNEED": 1879048190,
"DT_VERNEEDNUM": 1879048191,
"DT_LOPROC": 1879048192,
"DT_HIPROC": 2147483647
},
"size": 8
}
},
"base_types": {
@@ -958,9 +1086,9 @@
},
"metadata": {
"producer": {
"version": "0.0.1",
"name": "ikelos-by-hand",
"datetime": "2019-10-21T22:52:00"
"version": "0.0.2",
"name": "gcmoreira-by-hand",
"datetime": "2024-02-19T14:37:00"
},
"format": "6.1.0"
}
@@ -7,14 +7,13 @@ import logging
import socket as socket_module
from typing import Generator, Iterable, Iterator, Optional, Tuple, List
from volatility3.framework import constants
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY
from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS
from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS
from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES
from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES
from volatility3.framework.constants.linux import CAPABILITIES
from volatility3.framework import exceptions, objects, interfaces, symbols
from volatility3.framework.layers import linear
from volatility3.framework.objects import utility
from volatility3.framework.symbols import generic, linux, intermed
@@ -26,14 +25,58 @@ vollog = logging.getLogger(__name__)
class module(generic.GenericIntelProcess):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._mod_mem_type = None # Initialize _mod_mem_type to None for memoization
@property
def mod_mem_type(self):
"""Return the mod_mem_type enum choices if available or an empty dict if not"""
# mod_mem_type and module_memory were added in kernel 6.4 which replaces
# module_layout for storing the information around core_layout etc.
# see commit ac3b43283923440900b4f36ca5f9f0b1ca43b70e for more information
if self._mod_mem_type is None:
try:
self._mod_mem_type = self._context.symbol_space.get_enumeration(
self.get_symbol_table_name() + constants.BANG + "mod_mem_type"
).choices
except exceptions.SymbolError:
vollog.debug(
f"Unable to find mod_mem_type enum. This message can be ignored for kernels < 6.4"
)
# set to empty dict to show that the enum was not found, and so shouldn't be searched for again
self._mod_mem_type = {}
return self._mod_mem_type
def get_module_base(self):
if self.has_member("core_layout"):
if self.has_member("mem"): # kernels 6.4+
try:
return self.mem[self.mod_mem_type["MOD_TEXT"]].base
except KeyError:
raise AttributeError(
"module -> get_module_base: Unable to get module base. Cannot read base from MOD_TEXT."
)
elif self.has_member("core_layout"):
return self.core_layout.base
else:
elif self.has_member("module_core"):
return self.module_core
raise AttributeError("module -> get_module_base: Unable to get module base")
def get_init_size(self):
if self.has_member("init_layout"):
if self.has_member("mem"): # kernels 6.4+
try:
return (
self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].size
+ self.mem[self.mod_mem_type["MOD_INIT_DATA"]].size
+ self.mem[self.mod_mem_type["MOD_INIT_RODATA"]].size
)
except KeyError:
raise AttributeError(
"module -> get_init_size: Unable to determine .init section size of module. Cannot read size of MOD_INIT_TEXT, MOD_INIT_DATA, and MOD_INIT_RODATA"
)
elif self.has_member("init_layout"):
return self.init_layout.size
elif self.has_member("init_size"):
return self.init_size
@@ -42,7 +85,19 @@ class module(generic.GenericIntelProcess):
)
def get_core_size(self):
if self.has_member("core_layout"):
if self.has_member("mem"): # kernels 6.4+
try:
return (
self.mem[self.mod_mem_type["MOD_TEXT"]].size
+ self.mem[self.mod_mem_type["MOD_DATA"]].size
+ self.mem[self.mod_mem_type["MOD_RODATA"]].size
+ self.mem[self.mod_mem_type["MOD_RO_AFTER_INIT"]].size
)
except KeyError:
raise AttributeError(
"module -> get_core_size: Unable to determine core size of module. Cannot read size of MOD_TEXT, MOD_DATA, MOD_RODATA, and MOD_RO_AFTER_INIT."
)
elif self.has_member("core_layout"):
return self.core_layout.size
elif self.has_member("core_size"):
return self.core_size
@@ -51,18 +106,32 @@ class module(generic.GenericIntelProcess):
)
def get_module_core(self):
if self.has_member("core_layout"):
if self.has_member("mem"): # kernels 6.4+
try:
return self.mem[self.mod_mem_type["MOD_TEXT"]].base
except KeyError:
raise AttributeError(
"module -> get_module_core: Unable to get module core. Cannot read base from MOD_TEXT."
)
elif self.has_member("core_layout"):
return self.core_layout.base
elif self.has_member("module_core"):
return self.module_core
raise AttributeError("module -> get_module_core: Unable to get module core")
def get_module_init(self):
if self.has_member("init_layout"):
if self.has_member("mem"): # kernels 6.4+
try:
return self.mem[self.mod_mem_type["MOD_INIT_TEXT"]].base
except KeyError:
raise AttributeError(
"module -> get_module_core: Unable to get module init. Cannot read base from MOD_INIT_TEXT."
)
elif self.has_member("init_layout"):
return self.init_layout.base
elif self.has_member("module_init"):
return self.module_init
raise AttributeError("module -> get_module_core: Unable to get module init")
raise AttributeError("module -> get_module_init: Unable to get module init")
def get_name(self):
"""Get the name of the module as a string"""
@@ -637,7 +706,8 @@ class vm_area_struct(objects.StructType):
def get_page_offset(self) -> int:
if self.vm_file == 0:
return 0
return self.vm_pgoff << constants.linux.PAGE_SHIFT
parent_layer = self._context.layers[self.vol.layer_name]
return self.vm_pgoff << parent_layer.page_shift
def get_name(self, context, task):
if self.vm_file != 0:
@@ -666,7 +736,7 @@ class vm_area_struct(objects.StructType):
elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0:
ret = True
elif proclayer and "x" in flags_str:
for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT):
for i in range(self.vm_start, self.vm_end, proclayer.page_size):
try:
if proclayer.is_dirty(i):
vollog.warning(
@@ -1067,17 +1137,17 @@ class vfsmount(objects.StructType):
"""Helper to make sure it is comparing two pointers to 'vfsmount'.
Depending on the kernel version, the calling object (self) could be
a 'vfsmount \*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
a 'vfsmount \\*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust
in the framework "auto" dereferencing ability to assure that when we
reach this point 'self' will be a 'vfsmount' already and self.vol.offset
a 'vfsmount \*' and not a 'vfsmount \*\*'. The argument must be a 'vfsmount \*'.
a 'vfsmount \\*' and not a 'vfsmount \\*\\*'. The argument must be a 'vfsmount \\*'.
Typically, it's called from do_get_path().
Args:
vfsmount_ptr (vfsmount \*): A pointer to a 'vfsmount'
vfsmount_ptr (vfsmount *): A pointer to a 'vfsmount'
Raises:
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \*'
exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \\*'
Returns:
bool: 'True' if the given argument points to the the same 'vfsmount'
@@ -6,6 +6,10 @@ from typing import Dict, Tuple
import logging
from volatility3.framework import constants
from volatility3.framework.constants.linux import (
ELF_IDENT,
ELF_CLASS,
)
from volatility3.framework import objects, interfaces, exceptions
vollog = logging.getLogger(__name__)
@@ -59,13 +63,15 @@ class elf(objects.StructType):
ei_class = self._context.object(
symbol_table_name + constants.BANG + "unsigned char",
layer_name=layer_name,
offset=object_info.offset + 0x4,
offset=object_info.offset + ELF_IDENT.EI_CLASS,
)
if ei_class == 1:
if ei_class == ELF_CLASS.ELFCLASS32:
self._type_prefix = "Elf32_"
elif ei_class == 2:
self._ei_class_size = 32
elif ei_class == ELF_CLASS.ELFCLASS64:
self._type_prefix = "Elf64_"
self._ei_class_size = 64
else:
raise ValueError(f"Unsupported ei_class value {ei_class}")
@@ -140,36 +146,137 @@ class elf(objects.StructType):
)
return section_headers
def get_link_maps(self, kernel_symbol_table_name):
"""Get the ELF link map objects for the given VMA address
Args:
kernel_symbol_table_name (str): Kernel symbol table name
Yields:
The ELF link map objects
"""
got_entry_size = self._ei_class_size // 8
elf_symbol_table = self.get_symbol_table_name()
link_maps_seen = set()
for phdr in self.get_program_headers():
try:
if phdr.p_type.description != "PT_DYNAMIC":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
for dsec in phdr.dynamic_sections():
try:
if dsec.d_tag.description != "DT_PLTGOT":
continue
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF dynamic section type: {dsec.d_tag}",
)
continue
got_start = dsec.d_ptr
# link_map is stored at the second GOT entry
link_map_addr = got_start + got_entry_size
# It needs the kernel symbol table to create a pointer
link_map_ptr = self._context.object(
kernel_symbol_table_name + constants.BANG + "pointer",
offset=link_map_addr,
layer_name=self.vol.layer_name,
)
if not link_map_ptr:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid ELF link map pointer at 0x{link_map_addr:x}",
)
continue
linkmap_symname = (
elf_symbol_table + constants.BANG + self._type_prefix + "LinkMap"
)
try:
link_map = self._context.object(
object_type=linkmap_symname,
offset=link_map_ptr,
layer_name=self.vol.layer_name,
)
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid ELF link map address at 0x{link_map_ptr:x}",
)
continue
while link_map and link_map.vol.offset != 0:
if link_map.vol.offset in link_maps_seen:
break
link_maps_seen.add(link_map.vol.offset)
yield link_map
try:
link_map = self._context.object(
object_type=linkmap_symname,
offset=link_map.l_next,
layer_name=self.vol.layer_name,
)
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVVV,
f"ELF link map linked list is corrupt at 0x{self.vol.offset:x}",
)
break
def _find_symbols(self):
dt_strtab = None
dt_symtab = None
dt_strent = None
for phdr in self.get_program_headers():
# Find PT_DYNAMIC segment
try:
# Find PT_DYNAMIC segment
if str(phdr.p_type.description) != "PT_DYNAMIC":
if phdr.p_type.description != "PT_DYNAMIC":
continue
except ValueError:
# If the p_type value is outside the ones declared in the enumeration, an
# exception is raised
return None
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
# This section contains pointers to the strtab, symtab, and strent sections
for dsec in phdr.dynamic_sections():
if dsec.d_tag == 5:
try:
dtag = dsec.d_tag.description
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF dynamic section type: {dsec.d_tag}",
)
continue
if dtag == "DT_STRTAB":
dt_strtab = dsec.d_ptr
elif dsec.d_tag == 6:
elif dtag == "DT_SYMTAB":
dt_symtab = dsec.d_ptr
elif dsec.d_tag == 11:
elif dtag == "DT_SYMENT":
# Size of the symtab symbol entry
dt_strent = dsec.d_ptr
break
if dt_strtab is None or dt_symtab is None or dt_strent is None:
if not (dt_strtab and dt_symtab and dt_strent):
return None
self._cached_symtab = dt_symtab
@@ -274,19 +381,31 @@ class elf_phdr(objects.StructType):
def get_vaddr(self):
offset = self.__getattr__("p_vaddr")
if self._parent_e_type == 3: # ET_DYN
offset = self._parent_offset + offset
try:
if self._parent_e_type.description == "ET_DYN":
offset = self._parent_offset + offset
except ValueError:
# Unknown ELF object file type. Anyway, if the ELF object file type is not a
# shared object (ET_DYN), the virtual address is 'p_vaddr'.
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF object type: {self._parent_e_type}",
)
return offset
def dynamic_sections(self):
# sanity check
try:
if str(self.p_type.description) != "PT_DYNAMIC":
if self.p_type.description != "PT_DYNAMIC":
return None
except ValueError:
# If the value is outside the ones declared in the enumeration, an
# exception is raised
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {self.p_type}",
)
return None
# the buffer of array starts at elf_base + our virtual address ( offset )
@@ -314,10 +433,30 @@ class elf_phdr(objects.StructType):
break
class elf_linkmap(objects.StructType):
def get_name(self):
try:
buf = self._context.layers.read(self.vol.layer_name, self.l_name, 256)
except exceptions.PagedInvalidAddressException:
# Protection against memory smear
vollog.log(
constants.LOGLEVEL_VVVV,
f"Invalid l_name address for ELF link map at 0x{self.vol.offset:x}",
)
return None
idx = buf.find(b"\x00")
if idx != -1:
buf = buf[:idx]
return buf.decode()
class_types = {
"Elf": elf,
"Elf64_Phdr": elf_phdr,
"Elf32_Phdr": elf_phdr,
"Elf32_Sym": elf_sym,
"Elf64_Sym": elf_sym,
"Elf32_LinkMap": elf_linkmap,
"Elf64_LinkMap": elf_linkmap,
}
@@ -21,12 +21,14 @@ class MacKernelIntermedSymbols(intermed.IntermediateSymbolTable):
self.set_type_class("vm_map_object", extensions.vm_map_object)
self.set_type_class("socket", extensions.socket)
self.set_type_class("inpcb", extensions.inpcb)
self.set_type_class("queue_entry", extensions.queue_entry)
self.set_type_class("ifnet", extensions.ifnet)
self.set_type_class("sockaddr_dl", extensions.sockaddr_dl)
self.set_type_class("sockaddr", extensions.sockaddr)
self.set_type_class("sysctl_oid", extensions.sysctl_oid)
self.set_type_class("kauth_scope", extensions.kauth_scope)
# https://developer.apple.com/documentation/kernel/queue_head_t
self.set_type_class("queue_entry", extensions.queue_entry)
self.optional_set_type_class("queue_head_t", extensions.queue_entry)
class MacUtilities(interfaces.configuration.VersionableInterface):
@@ -490,22 +490,24 @@ class queue_entry(objects.StructType):
for attr in ["next", "prev"]:
with contextlib.suppress(exceptions.InvalidAddressException):
n = getattr(self, attr).dereference().cast(type_name)
while n is not None and n.vol.offset != list_head:
if n.vol.offset in seen:
queue_element = getattr(self, attr).dereference().cast(type_name)
while (
queue_element is not None
and queue_element.vol.offset != list_head.vol.offset
):
if queue_element.vol.offset in seen:
break
yield n
yield queue_element
seen.add(n.vol.offset)
seen.add(queue_element.vol.offset)
yielded = yielded + 1
if yielded == max_size:
return None
n = (
getattr(n.member(attr=member_name), attr)
queue_element = (
getattr(queue_element.member(attr=member_name), attr)
.dereference()
.cast(type_name)
)
@@ -492,9 +492,47 @@ class KMUTANT(objects.StructType, pool.ExecutiveObject):
return header.NameInfo.Name.String # type: ignore
class ETHREAD(objects.StructType):
class ETHREAD(objects.StructType, pool.ExecutiveObject):
"""A class for executive thread objects."""
def is_valid(self) -> bool:
"""Determine if the object is valid."""
try:
# validation by TID:
if self.Cid.UniqueThread % 4 != 0: # NT tids are divisible by 4
return False
# validation by PID of parent process:
if self.Cid.UniqueProcess % 4 != 0:
return False
# validation by thread creation time:
if (
self.Cid.UniqueProcess != 4
): # The System process (PID 4) has no create time
ctime = self.get_create_time()
if not isinstance(ctime, datetime.datetime):
return False
if not (1998 < ctime.year < 2030):
return False
except exceptions.InvalidAddressException:
return False
# passed all validations
return True
def get_create_time(self):
# For Windows XPs
if self.has_member("ThreadsProcess"):
return conversion.wintime_to_datetime(self.CreateTime.QuadPart >> 3)
return conversion.wintime_to_datetime(self.CreateTime.QuadPart)
def get_exit_time(self):
return conversion.wintime_to_datetime(self.ExitTime.QuadPart)
def owning_process(self) -> interfaces.objects.ObjectInterface:
"""Return the EPROCESS that owns this thread."""