Merge branch 'develop' into linux_lsof_refactoring_fixes_and_improvements

This commit is contained in:
Gustavo Moreira
2024-10-04 12:08:10 +10:00
22 changed files with 2080 additions and 131 deletions
+2 -1
View File
@@ -134,4 +134,5 @@ def __getattr__(name):
]:
warnings.warn(f"{name} is deprecated", FutureWarning)
return globals()[f"{deprecated_tag}{name}"]
return None
return getattr(__import__(__name__), name)
@@ -494,8 +494,7 @@ class SimpleTypeRequirement(RequirementInterface):
"""Validates the instance requirement based upon its
`instance_type`."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
if not isinstance(value, self.instance_type):
vollog.log(
constants.LOGLEVEL_V,
@@ -536,7 +535,7 @@ class ClassRequirement(RequirementInterface):
"""Checks to see if a class can be recovered."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
self._cls = None
if value is not None and isinstance(value, str):
if "." in value:
@@ -2,20 +2,19 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from volatility3.framework import interfaces, renderers
from volatility3.framework.renderers import format_hints
from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
class Check_creds(interfaces.plugins.PluginInterface):
"""Checks if any processes are sharing credential structures"""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
return [
@@ -46,20 +45,28 @@ class Check_creds(interfaces.plugins.PluginInterface):
tasks = pslist.PsList.list_tasks(self.context, vmlinux.name)
for task in tasks:
cred_addr = task.cred.dereference().vol.offset
task_cred_ptr = task.cred
if not (task_cred_ptr and task_cred_ptr.is_readable()):
continue
if cred_addr not in creds:
creds[cred_addr] = []
cred_addr = task_cred_ptr.dereference().vol.offset
creds.setdefault(cred_addr, [])
creds[cred_addr].append(task.pid)
for _, pids in creds.items():
for cred_addr, pids in creds.items():
if len(pids) > 1:
pid_str = ""
for pid in pids:
pid_str = pid_str + f"{pid:d}, "
pid_str = pid_str[:-2]
yield (0, [str(pid_str)])
pid_str = ", ".join([str(pid) for pid in pids])
fields = [
format_hints.Hex(cred_addr),
pid_str,
]
yield (0, fields)
def run(self):
return renderers.TreeGrid([("PIDs", str)], self._generator())
headers = [
("CredVAddr", format_hints.Hex),
("PIDs", str),
]
return renderers.TreeGrid(headers, self._generator())
+4 -10
View File
@@ -5,7 +5,7 @@
from typing import List
import logging
from volatility3.framework import constants, interfaces
from volatility3.framework import renderers
from volatility3.framework import renderers, symbols
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
@@ -63,15 +63,9 @@ class Malfind(interfaces.plugins.PluginInterface):
def _generator(self, tasks):
# determine if we're on a 32 or 64 bit kernel
vmlinux = self.context.modules[self.config["kernel"]]
if (
self.context.symbol_space.get_type(
vmlinux.symbol_table_name + constants.BANG + "pointer"
).size
== 4
):
is_32bit_arch = True
else:
is_32bit_arch = False
is_32bit_arch = not symbols.symbol_table_is_64bit(
self.context, vmlinux.symbol_table_name
)
for task in tasks:
process_name = utility.array_to_string(task.comm)
@@ -20,7 +20,7 @@ class PIDHashTable(plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
_version = (1, 0, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -45,9 +45,7 @@ class PIDHashTable(plugins.PluginInterface):
]
def _is_valid_task(self, task) -> bool:
vmlinux = self.context.modules[self.config["kernel"]]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
return bool(task and task.pid > 0 and vmlinux_layer.is_valid(task.parent))
return bool(task and task.pid > 0 and task.parent.is_readable())
def _get_pidtype_pid(self):
vmlinux = self.context.modules[self.config["kernel"]]
@@ -96,7 +94,7 @@ class PIDHashTable(plugins.PluginInterface):
seen_upids.add(upid.vol.offset)
pid_chain = upid.pid_chain
if not (pid_chain and vmlinux_layer.is_valid(pid_chain.vol.offset)):
if not (pid_chain.next and pid_chain.next.is_readable()):
break
upid = linux.LinuxUtilities.container_of(
@@ -105,7 +103,6 @@ class PIDHashTable(plugins.PluginInterface):
def _get_upids(self):
vmlinux = self.context.modules[self.config["kernel"]]
vmlinux_layer = self.context.layers[vmlinux.layer_name]
# 2.6.24 <= kernels < 4.15
pidhash = self._get_pidhash_array()
@@ -115,7 +112,7 @@ class PIDHashTable(plugins.PluginInterface):
# each entry in the hlist is a upid which is wrapped in a pid
ent = hlist.first
while ent and vmlinux_layer.is_valid(ent.vol.offset):
while ent and ent.is_readable():
# upid->pid_chain exists 2.6.24 <= kernel < 4.15
upid = linux.LinuxUtilities.container_of(
ent.vol.offset, "upid", "pid_chain", vmlinux
@@ -143,7 +140,7 @@ class PIDHashTable(plugins.PluginInterface):
continue
pid_tasks_0 = pid.tasks[pidtype_pid].first
if not pid_tasks_0:
if not (pid_tasks_0 and pid_tasks_0.is_readable()):
continue
task = vmlinux.object(
@@ -160,7 +157,7 @@ class PIDHashTable(plugins.PluginInterface):
pidtype_pid = self._get_pidtype_pid()
pid_tasks_0 = pid.tasks[pidtype_pid].first
if not pid_tasks_0:
if not (pid_tasks_0 and pid_tasks_0.is_readable()):
return None
task_struct_type = vmlinux.get_type("task_struct")
@@ -22,7 +22,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (2, 0, 0)
def __init__(self, vmlinux, task, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -450,7 +450,7 @@ class Sockstat(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="SockHandlers", component=SockHandlers, version=(1, 0, 0)
name="SockHandlers", component=SockHandlers, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="lsof", plugin=lsof.Lsof, version=(2, 0, 0)
@@ -550,7 +550,7 @@ class Sockstat(plugins.PluginInterface):
except AttributeError:
netns_id = NotAvailableValue()
yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields
yield task_comm, task, netns_id, fd_num, family, sock_type, protocol, sock_fields
def _format_fields(self, sock_stat, protocol):
"""Prepare the socket fields to be rendered
@@ -597,6 +597,7 @@ class Sockstat(plugins.PluginInterface):
)
for (
task_comm,
task,
netns_id,
fd_num,
@@ -619,6 +620,7 @@ class Sockstat(plugins.PluginInterface):
fields = (
netns_id,
task_comm,
task.tgid,
task.pid,
fd_num,
@@ -639,6 +641,7 @@ class Sockstat(plugins.PluginInterface):
tree_grid_args = [
("NetNS", int),
("Process Name", str),
("PID", int),
("TID", int),
("FD", int),
+18 -11
View File
@@ -45,6 +45,7 @@ class Timeliner(interfaces.plugins.PluginInterface):
orders the results by time."""
_required_framework_version = (2, 0, 0)
_version = (1, 1, 0)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -198,9 +199,10 @@ class Timeliner(interfaces.plugins.PluginInterface):
),
)
)
except Exception:
except Exception as e:
vollog.log(
logging.INFO, f"Exception occurred running plugin: {plugin_name}"
logging.INFO,
f"Exception occurred running plugin: {plugin_name}: {e}",
)
vollog.log(logging.DEBUG, traceback.format_exc())
@@ -245,6 +247,18 @@ class Timeliner(interfaces.plugins.PluginInterface):
filter_list = self.config["plugin-filter"]
# Identify plugins that we can run which output datetimes
for plugin_class in self.usable_plugins:
if not issubclass(plugin_class, TimeLinerInterface):
# get_usable_plugins() should filter this, but adding a safeguard just in case
continue
if filter_list and not any(
[
filter in plugin_class.__module__ + "." + plugin_class.__name__
for filter in filter_list
]
):
continue
try:
automagics = automagic.choose_automagic(self.automagics, plugin_class)
@@ -276,15 +290,8 @@ class Timeliner(interfaces.plugins.PluginInterface):
config_value,
)
if isinstance(plugin, TimeLinerInterface):
if not len(filter_list) or any(
[
filter
in plugin.__module__ + "." + plugin.__class__.__name__
for filter in filter_list
]
):
plugins_to_run.append(plugin)
plugins_to_run.append(plugin)
except exceptions.UnsatisfiedException as excp:
# Remove the failed plugin from the list and continue
vollog.debug(
@@ -248,8 +248,12 @@ class Callbacks(interfaces.plugins.PluginInterface):
context, layer_name, nt_symbol_table, constraints
):
try:
if hasattr(mem_object, "is_valid") and not mem_object.is_valid():
continue
if isinstance(mem_object, callbacks._SHUTDOWN_PACKET):
if not mem_object.is_parseable(type_map):
continue
elif hasattr(mem_object, "is_valid"):
if not mem_object.is_valid():
continue
yield cls._process_scanned_callback(mem_object, type_map)
except exceptions.InvalidAddressException:
@@ -0,0 +1,211 @@
# 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
# Full details on the techniques used in these plugins to detect EDR-evading malware
# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation
# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf
import logging
from typing import Tuple, Optional, Generator, List, Dict
from functools import partial
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints
import volatility3.plugins.windows.pslist as pslist
import volatility3.plugins.windows.threads as threads
import volatility3.plugins.windows.pe_symbols as pe_symbols
vollog = logging.getLogger(__name__)
class DebugRegisters(interfaces.plugins.PluginInterface):
# 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) -> List:
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="pe_symbols", component=pe_symbols.PESymbols, version=(1, 0, 0)
),
]
@staticmethod
def _get_debug_info(
ethread: interfaces.objects.ObjectInterface,
) -> Optional[Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]]:
"""
Gathers information related to the debug registers for the given thread
Args:
ethread: the thread (_ETHREAD) to examine
Returns:
Tuple[interfaces.objects.ObjectInterface, int, int, int, int, int]: The owner process of the thread and the values for dr7, dr0, dr1, dr2, dr3
"""
try:
dr7 = ethread.Tcb.TrapFrame.Dr7
state = ethread.Tcb.State
except exceptions.InvalidAddressException:
return None
# 0 = debug registers not active
# 4 = terminated
if dr7 == 0 or state == 4:
return None
try:
owner_proc = ethread.owning_process()
except (AttributeError, exceptions.InvalidAddressException):
return None
dr0 = ethread.Tcb.TrapFrame.Dr0
dr1 = ethread.Tcb.TrapFrame.Dr1
dr2 = ethread.Tcb.TrapFrame.Dr2
dr3 = ethread.Tcb.TrapFrame.Dr3
# bail if all are 0
if not (dr0 or dr1 or dr2 or dr3):
return None
return owner_proc, dr7, dr0, dr1, dr2, dr3
def _generator(
self,
) -> Generator[
Tuple[
int,
Tuple[
str,
int,
int,
int,
int,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
format_hints.Hex,
str,
str,
],
],
None,
None,
]:
kernel = self.context.modules[self.config["kernel"]]
vads_cache: Dict[int, pe_symbols.ranges_type] = {}
proc_modules = None
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
)
for proc in procs:
for thread in threads.Threads.list_threads(kernel, proc):
debug_info = self._get_debug_info(thread)
if not debug_info:
continue
owner_proc, dr7, dr0, dr1, dr2, dr3 = debug_info
vads = pe_symbols.PESymbols.get_vads_for_process_cache(
vads_cache, owner_proc
)
if not vads:
continue
# this lookup takes a while, so only perform if we need to
if not proc_modules:
proc_modules = pe_symbols.PESymbols.get_process_modules(
self.context, kernel.layer_name, kernel.symbol_table_name, None
)
path_and_symbol = partial(
pe_symbols.PESymbols.path_and_symbol_for_address,
self.context,
self.config_path,
proc_modules,
)
file0, sym0 = path_and_symbol(vads, dr0)
file1, sym1 = path_and_symbol(vads, dr1)
file2, sym2 = path_and_symbol(vads, dr2)
file3, sym3 = path_and_symbol(vads, dr3)
# if none map to an actual file VAD then bail
if not (file0 or file1 or file2 or file3):
continue
process_name = owner_proc.ImageFileName.cast(
"string",
max_length=owner_proc.ImageFileName.vol.count,
errors="replace",
)
thread_tid = thread.Cid.UniqueThread
yield (
0,
(
process_name,
owner_proc.UniqueProcessId,
thread_tid,
thread.Tcb.State,
dr7,
format_hints.Hex(dr0),
file0 or renderers.NotApplicableValue(),
sym0 or renderers.NotApplicableValue(),
format_hints.Hex(dr1),
file1 or renderers.NotApplicableValue(),
sym1 or renderers.NotApplicableValue(),
format_hints.Hex(dr2),
file2 or renderers.NotApplicableValue(),
sym2 or renderers.NotApplicableValue(),
format_hints.Hex(dr3),
file3 or renderers.NotApplicableValue(),
sym3 or renderers.NotApplicableValue(),
),
)
def run(self) -> renderers.TreeGrid:
return renderers.TreeGrid(
[
("Process", str),
("PID", int),
("TID", int),
("State", int),
("Dr7", int),
("Dr0", format_hints.Hex),
("Range0", str),
("Symbol0", str),
("Dr1", format_hints.Hex),
("Range1", str),
("Symbol1", str),
("Dr2", format_hints.Hex),
("Range2", str),
("Symbol2", str),
("Dr3", format_hints.Hex),
("Range3", str),
("Symbol3", str),
],
self._generator(),
)
@@ -76,7 +76,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
# ~ vollog.debug("Using pool size constraints: TcpL {}, TcpE {}, UdpA {}".format(tcpl_size, tcpe_size, udpa_size))
return [
constraints = [
# TCP listener
poolscanner.PoolConstraint(
b"TcpL",
@@ -100,6 +100,19 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
),
]
if symbol_table.startswith("netscan-win10-20348"):
vollog.debug("Adding additional pool constraint for `TTcb` tags")
constraints.append(
poolscanner.PoolConstraint(
b"TTcb",
type_name=symbol_table + constants.BANG + "_TCP_ENDPOINT",
size=(tcpe_size, None),
page_type=poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE,
)
)
return constraints
@classmethod
def determine_tcpip_version(
cls,
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
import datetime
import logging
from typing import Callable, Iterable, List, Type
from typing import Callable, Iterator, List, Type
from volatility3.framework import renderers, interfaces, layers, exceptions, constants
from volatility3.framework.configuration import requirements
@@ -12,6 +12,7 @@ from volatility3.framework.objects import utility
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.windows.extensions import pe
from volatility3.framework.symbols.windows import extensions
from volatility3.plugins import timeliner
vollog = logging.getLogger(__name__)
@@ -197,7 +198,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
filter_func: Callable[
[interfaces.objects.ObjectInterface], bool
] = lambda _: False,
) -> Iterable[interfaces.objects.ObjectInterface]:
) -> Iterator["extensions.EPROCESS"]:
"""Lists all the processes in the primary layer that are in the pid
config option.
@@ -1,9 +1,14 @@
import datetime, logging, string
import datetime
import logging
import string
from itertools import chain
from typing import Dict, Iterable, List
from volatility3.framework import constants, exceptions
from volatility3.framework.interfaces import plugins
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import format_hints, TreeGrid
from volatility3.framework.interfaces import plugins
from volatility3.framework.renderers import TreeGrid, format_hints
from volatility3.framework.symbols.windows import extensions
from volatility3.plugins.windows import (
handles,
info,
@@ -71,20 +76,19 @@ class PsXView(plugins.PluginInterface):
"string", max_length=proc.ImageFileName.vol.count, errors="replace"
)
def _is_valid_proc_name(self, str):
for c in str:
if not c in self.valid_proc_name_chars:
return False
return True
def _is_valid_proc_name(self, string: str) -> bool:
return all(c in self.valid_proc_name_chars for c in string)
def _filter_garbage_procs(self, proc_list):
def _filter_garbage_procs(
self, proc_list: Iterable[extensions.EPROCESS]
) -> List[extensions.EPROCESS]:
return [
p
for p in proc_list
if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p))
]
def _translate_offset(self, offset):
def _translate_offset(self, offset: int) -> int:
if not self.config["physical-offsets"]:
return offset
@@ -100,21 +104,25 @@ class PsXView(plugins.PluginInterface):
return offset
def _proc_list_to_dict(self, tasks):
def _proc_list_to_dict(
self, tasks: Iterable[extensions.EPROCESS]
) -> Dict[int, extensions.EPROCESS]:
tasks = self._filter_garbage_procs(tasks)
return {self._translate_offset(proc.vol.offset): proc for proc in tasks}
def _check_pslist(self, tasks):
return self._proc_list_to_dict(tasks)
def _check_psscan(self, layer_name, symbol_table):
def _check_psscan(
self, layer_name: str, symbol_table: str
) -> Dict[int, extensions.EPROCESS]:
res = psscan.PsScan.scan_processes(
context=self.context, layer_name=layer_name, symbol_table=symbol_table
)
return self._proc_list_to_dict(res)
def _check_thrdscan(self):
def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]:
ret = []
for ethread in thrdscan.ThrdScan.scan_threads(
@@ -135,33 +143,38 @@ class PsXView(plugins.PluginInterface):
return self._proc_list_to_dict(ret)
def _check_csrss_handles(self, tasks, layer_name, symbol_table):
ret = []
def _check_csrss_handles(
self, tasks: Iterable[extensions.EPROCESS], layer_name: str, symbol_table: str
) -> Dict[int, extensions.EPROCESS]:
ret: List[extensions.EPROCESS] = []
handles_plugin = handles.Handles(
context=self.context, config_path=self.config_path
)
type_map = handles_plugin.get_type_map(self.context, layer_name, symbol_table)
cookie = handles_plugin.find_cookie(
context=self.context,
layer_name=layer_name,
symbol_table=symbol_table,
)
for p in tasks:
name = self._proc_name_to_string(p)
if name == "csrss.exe":
try:
if p.has_member("ObjectTable"):
handles_plugin = handles.Handles(
context=self.context, config_path=self.config_path
)
hndls = list(handles_plugin.handles(p.ObjectTable))
for h in hndls:
if (
h.get_object_type(
handles_plugin.get_type_map(
self.context, layer_name, symbol_table
)
)
== "Process"
):
ret.append(h.Body.cast("_EPROCESS"))
if name != "csrss.exe":
continue
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVV, "Cannot access eprocess object table"
)
try:
ret += [
handle.Body.cast("_EPROCESS")
for handle in handles_plugin.handles(p.ObjectTable)
if handle.get_object_type(type_map, cookie) == "Process"
]
except exceptions.InvalidAddressException:
vollog.log(
constants.LOGLEVEL_VVV, "Cannot access eprocess object table"
)
return self._proc_list_to_dict(ret)
@@ -178,7 +191,7 @@ class PsXView(plugins.PluginInterface):
)
# get processes from each source
processes = {}
processes: Dict[str, Dict[int, extensions.EPROCESS]] = {}
processes["pslist"] = self._check_pslist(kdbg_list_processes)
processes["psscan"] = self._check_psscan(layer_name, symbol_table)
@@ -187,27 +200,20 @@ class PsXView(plugins.PluginInterface):
kdbg_list_processes, layer_name, symbol_table
)
# print results
# list of lists of offsets
offsets = [list(processes[source].keys()) for source in processes]
# flatten to one list
offsets = sum(offsets, [])
# remove duplicates
offsets = set(offsets)
# Unique set of all offsets from all sources
offsets = set(chain(*(mapping.keys() for mapping in processes.values())))
for offset in offsets:
proc = None
# We know there will be at least one process mapped to each offset
proc: extensions.EPROCESS = next(
mapping[offset] for mapping in processes.values() if offset in mapping
)
in_sources = {src: False for src in processes}
for source in processes:
if offset in processes[source]:
for source, process_mapping in processes.items():
if offset in process_mapping:
in_sources[source] = True
if not proc:
proc = processes[source][offset]
pid = proc.UniqueProcessId
name = self._proc_name_to_string(proc)
@@ -0,0 +1,208 @@
# 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
# Full details on the techniques used in these plugins to detect EDR-evading malware
# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation
# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf
import logging
from typing import Dict, Tuple, List, Generator
from volatility3.framework import interfaces, exceptions
from volatility3.framework import renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.plugins.windows import pslist, pe_symbols
vollog = logging.getLogger(__name__)
class unhooked_system_calls(interfaces.plugins.PluginInterface):
"""Looks for signs of Skeleton Key malware"""
_required_framework_version = (2, 4, 0)
system_calls = {
"ntdll.dll": {
pe_symbols.wanted_names_identifier: [
"NtCreateThread",
"NtProtectVirtualMemory",
"NtReadVirtualMemory",
"NtOpenProcess",
"NtWriteFile",
"NtQueryVirtualMemory",
"NtAllocateVirtualMemory",
"NtWorkerFactoryWorkerReady",
"NtAcceptConnectPort",
"NtAddDriverEntry",
"NtAdjustPrivilegesToken",
"NtAlpcCreatePort",
"NtClose",
"NtCreateFile",
"NtCreateMutant",
"NtOpenFile",
"NtOpenIoCompletion",
"NtOpenJobObject",
"NtOpenKey",
"NtOpenKeyEx",
"NtOpenThread",
"NtOpenThreadToken",
"NtOpenThreadTokenEx",
"NtWriteVirtualMemory",
"NtTraceEvent",
"NtTranslateFilePath",
"NtUmsThreadYield",
"NtUnloadDriver",
"NtUnloadKey",
"NtUnloadKey2",
"NtUnloadKeyEx",
"NtCreateKey",
"NtCreateSection",
"NtDeleteKey",
"NtDeleteValueKey",
"NtDuplicateObject",
"NtQueryValueKey",
"NtReplaceKey",
"NtRequestWaitReplyPort",
"NtRestoreKey",
"NtSetContextThread",
"NtSetSecurityObject",
"NtSetValueKey",
"NtSystemDebugControl",
"NtTerminateProcess",
]
}
}
# This data structure is used to track unique implementations of functions across processes
# The outer dictionary holds the module name (e.g., ntdll.dll)
# The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module
# The innermost dictionary holds the unique implementation (bytes) of a function across processes
# Each implementation is tracked along with the process(es) that host it
# For systems without malware, all functions should have the same implementation
# When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations
_code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]]
@classmethod
def get_requirements(cls) -> List:
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(
name="kernel",
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="pe_symbols", plugin=pe_symbols.PESymbols, version=(1, 0, 0)
),
]
def _gather_code_bytes(
self,
kernel: interfaces.context.ModuleInterface,
found_symbols: pe_symbols.found_symbols_type,
) -> _code_bytes_type:
"""
Enumerates the desired DLLs and function implementations in each process
Groups based on unique implementations of each DLLs' functions
The purpose is to detect when a function has different implementations (code)
in different processes.
This very effectively detects code injection.
"""
code_bytes: unhooked_system_calls._code_bytes_type = {}
procs = pslist.PsList.list_processes(
context=self.context,
layer_name=kernel.layer_name,
symbol_table=kernel.symbol_table_name,
)
for proc in procs:
try:
proc_id = proc.UniqueProcessId
proc_name = utility.array_to_string(proc.ImageFileName)
proc_layer_name = proc.add_process_layer()
except exceptions.InvalidAddressException:
continue
for dll_name, functions in found_symbols.items():
for func_name, func_addr in functions:
try:
fbytes = self.context.layers[proc_layer_name].read(
func_addr, 0x20
)
except exceptions.InvalidAddressException:
continue
# see the definition of _code_bytes_type for details of this data structure
if dll_name not in code_bytes:
code_bytes[dll_name] = {}
if func_name not in code_bytes[dll_name]:
code_bytes[dll_name][func_name] = {}
if fbytes not in code_bytes[dll_name][func_name]:
code_bytes[dll_name][func_name][fbytes] = []
code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name))
return code_bytes
def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]:
kernel = self.context.modules[self.config["kernel"]]
found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols(
self.context,
self.config_path,
kernel.layer_name,
kernel.symbol_table_name,
unhooked_system_calls.system_calls,
)
# code_bytes[dll_name][func_name][func_bytes]
code_bytes = self._gather_code_bytes(kernel, found_symbols)
# walk the functions that were evaluated
for functions in code_bytes.values():
# cbb is the distinct groups of bytes (instructions)
# for this function across processes
for func_name, cbb in functions.items():
# the dict key here is the raw instructions, which is not helpful to look at
# the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions)
cb = list(cbb.values())
# if all processes map to the same implementation, then no malware is present
if len(cb) == 1:
yield 0, (func_name, "", len(cb[0]))
else:
# if there are differing implementations then it means
# that malware has overwritten system call(s) in infected processes
# max_idx and small_idx find which implementation of a system call has the least processes
# as all observed malware and open source projects only infected a few targets, leaving the
# rest with the original EDR hooks in place
max_idx = 0 if len(cb[0]) > len(cb[1]) else 1
small_idx = (~max_idx) & 1
ps = []
# gather processes on small_idx since these are the malware infected ones
for pid, pname in cb[small_idx]:
ps.append("{:d}:{}".format(pid, pname))
proc_names = ", ".join(ps)
yield 0, (func_name, proc_names, len(cb[max_idx]))
def run(self) -> renderers.TreeGrid:
return renderers.TreeGrid(
[
("Function", str),
("Distinct Implementations", str),
("Total Implementations", int),
],
self._generator(),
)
@@ -3,7 +3,7 @@
#
import logging
from typing import Callable, List, Generator, Iterable, Type, Optional
from typing import Callable, List, Generator, Iterable, Type, Optional, Tuple
from volatility3.framework import renderers, interfaces, exceptions
from volatility3.framework.configuration import requirements
@@ -196,11 +196,31 @@ class VadInfo(interfaces.plugins.PluginInterface):
return file_handle
def _generator(self, procs):
def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[
Tuple[
int,
Tuple[
int,
str,
format_hints.Hex,
format_hints.Hex,
format_hints.Hex,
str,
str,
int,
int,
format_hints.Hex,
str,
str,
],
],
None,
None,
]:
kernel = self.context.modules[self.config["kernel"]]
kernel_layer = self.context.layers[kernel.layer_name]
def passthrough(_: interfaces.objects.ObjectInterface) -> bool:
def passthrough(x: interfaces.objects.ObjectInterface) -> bool:
return False
filter_func = passthrough
@@ -250,7 +270,7 @@ class VadInfo(interfaces.plugins.PluginInterface):
),
)
def run(self):
def run(self) -> renderers.TreeGrid:
kernel = self.context.modules[self.config["kernel"]]
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
@@ -18,7 +18,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
"""Scans all the Virtual Address Descriptor memory maps using yara."""
_required_framework_version = (2, 4, 0)
_version = (1, 1, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -68,7 +68,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface):
layer = self.context.layers[layer_name]
for start, size in self.get_vad_maps(task):
if size > sanity_check:
vollog.warn(
vollog.debug(
f"VAD at 0x{start:x} over sanity-check size, not scanning"
)
continue
@@ -48,9 +48,6 @@ class VerInfo(interfaces.plugins.PluginInterface):
requirements.PluginRequirement(
name="modules", plugin=modules.Modules, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="dlllist", component=dlllist.DllList, version=(2, 0, 0)
),
requirements.BooleanRequirement(
name="extensive",
description="Search physical layer for version information",
@@ -169,13 +169,30 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
Returns:
str: Sock pipe pathname relative to the task's root directory.
"""
# FIXME: This function must be moved to the 'dentry' object extension
# Also, the scope of this function went beyond the sock pipe path, so we need to rename this.
# Once https://github.com/volatilityfoundation/volatility3/pull/1263 is merged, replace the
# dentry inode getters
if not (filp and filp.is_readable()):
return f"<invalid file pointer> {filp:x}"
dentry = filp.get_dentry()
if not (dentry and dentry.is_readable()):
return f"<invalid dentry pointer> {dentry:x}"
kernel_module = cls.get_module_from_volobj_type(context, dentry)
sym_addr = dentry.d_op.d_dname
if not (sym_addr and sym_addr.is_readable()):
return f"<invalid d_dname pointer> {sym_addr:x}"
symbs = list(kernel_module.get_symbols_by_absolute_location(sym_addr))
inode = dentry.d_inode
if not (inode and inode.is_readable() and inode.is_valid()):
return f"<invalid dentry inode> {inode:x}"
if len(symbs) == 1:
sym = symbs[0].split(constants.BANG)[1]
@@ -191,15 +208,41 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface):
elif sym == "simple_dname":
pre_name = cls._get_path_file(task, filp)
elif sym == "ns_dname":
# From Kernels 3.19
# In Kernels >= 6.9, see Linux kernel commit 1fa08aece42512be072351f482096d5796edf7ca
# ns_common->stashed change from 'atomic64_t' to 'dentry*'
try:
ns_common_type = kernel_module.get_type("ns_common")
stashed_template = ns_common_type.child_template("stashed")
stashed_type_full_name = stashed_template.vol.type_name
stashed_type_name = stashed_type_full_name.split(constants.BANG)[1]
if stashed_type_name == "atomic64_t":
# 3.19 <= Kernels < 6.9
fsdata_ptr = dentry.d_fsdata
if not (fsdata_ptr and fsdata_ptr.is_readable()):
raise IndexError
ns_ops = fsdata_ptr.dereference().cast("proc_ns_operations")
else:
# Kernels >= 6.9
private_ptr = inode.i_private
if not (private_ptr and private_ptr.is_readable()):
raise IndexError
ns_common = private_ptr.dereference().cast("ns_common")
ns_ops = ns_common.ops
pre_name = utility.pointer_to_string(ns_ops.name, 255)
except IndexError:
pre_name = "<unsupported ns_dname implementation>"
else:
pre_name = f"<unsupported d_op symbol: {sym}>"
ret = f"{pre_name}:[{dentry.d_inode.i_ino:d}]"
pre_name = f"<unsupported d_op symbol> {sym}"
else:
ret = f"<invalid d_dname pointer> {sym_addr:x}"
pre_name = f"<unknown d_dname pointer> {sym_addr:x}"
return ret
return f"{pre_name}:[{inode.i_ino:d}]"
@classmethod
def path_for_file(cls, context, task, filp) -> str:
@@ -1,4 +1,5 @@
import logging
from typing import Dict
from volatility3.framework import exceptions, objects
from volatility3.framework.symbols.windows.extensions import pool
@@ -24,12 +25,8 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject):
and self.Entry.Blink.is_readable()
and self.DeviceObject.is_readable()
):
return False
device = self.DeviceObject
if not device or not (device.DriverObject.DriverStart % 0x1000 == 0):
vollog.debug(
f"callback obj 0x{self.vol.offset:x} invalid due to invalid device object"
f"Callback obj 0x{self.vol.offset:x} invalid due to unreadable structure members"
)
return False
@@ -39,12 +36,43 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject):
)
return False
return True
def is_parseable(self, type_map: Dict[int, str]) -> bool:
"""
Determines whether or not this `_SHUTDOWN_PACKET` callback can be reliably parsed.
Requires a `type_map` that maps NT executive object type indices to string representations.
This type map can be acquired via the `handles.Handles.get_type_map` classmethod.
"""
if not self.is_valid():
return False
try:
device = self.DeviceObject
if not device or not (device.DriverObject.DriverStart % 0x1000 == 0):
vollog.debug(
f"callback obj 0x{self.vol.offset:x} invalid due to invalid device object"
)
return False
header = device.get_object_header()
valid = header.NameInfo.Name == "Device"
return valid
object_type = header.get_object_type(type_map)
is_valid = object_type == "Device"
if not is_valid:
vollog.debug(
f"Callback obj 0x{self.vol.offset:x} invalid due to invalid device type: wanted 'Device', found '{object_type}'"
)
return is_valid
except exceptions.InvalidAddressException:
vollog.debug(
f"callback obj 0x{self.vol.offset:x} invalid due to invalid address access"
)
return False
except ValueError:
vollog.debug(f"Could not get NameInfo for object at 0x{self.vol.offset:x}")
vollog.debug(
f"Could not get object type for object at 0x{self.vol.offset:x}"
)
return False
@@ -0,0 +1,131 @@
{
"base_types": {
"pointer": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned char": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 1
},
"unsigned long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 4
},
"unsigned long long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned short": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 2
}
},
"enums": {},
"metadata": {
"format": "6.1.0",
"producer": {
"datetime": "2021-07-31T17:37:28.302702",
"name": "vmextract-by-hand",
"version": "0.0.1"
}
},
"symbols": {
"revision_id": {
"address": 0,
"constant_data": "MTQ="
}
},
"user_types": {
"_VMCS": {
"fields": {
"ept": {
"offset": 232,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"executive_vmcs_ptr": {
"offset": 208,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr3": {
"offset": 736,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr4": {
"offset": 744,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_pdpte": {
"offset": 928,
"type": {
"count": 4,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "unsigned long long"
}
}
},
"guest_physical_addr": {
"offset": 240,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr3": {
"offset": 832,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr4": {
"offset": 840,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vmcs_link_ptr": {
"offset": 248,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vpid": {
"offset": 752,
"type": {
"kind": "struct",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 4096
}
}
}
@@ -0,0 +1,131 @@
{
"base_types": {
"pointer": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned char": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 1
},
"unsigned long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 4
},
"unsigned long long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned short": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 2
}
},
"enums": {},
"metadata": {
"format": "6.1.0",
"producer": {
"datetime": "2021-07-31T17:37:28.311608",
"name": "vmextract-by-hand",
"version": "0.0.1"
}
},
"symbols": {
"revision_id": {
"address": 0,
"constant_data": "MTY="
}
},
"user_types": {
"_VMCS": {
"fields": {
"ept": {
"offset": 232,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"executive_vmcs_ptr": {
"offset": 208,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr3": {
"offset": 736,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr4": {
"offset": 744,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_pdpte": {
"offset": 928,
"type": {
"count": 4,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "unsigned long long"
}
}
},
"guest_physical_addr": {
"offset": 240,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr3": {
"offset": 832,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr4": {
"offset": 840,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vmcs_link_ptr": {
"offset": 248,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vpid": {
"offset": 752,
"type": {
"kind": "struct",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 4096
}
}
}
@@ -0,0 +1,131 @@
{
"base_types": {
"pointer": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned char": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 1
},
"unsigned long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 4
},
"unsigned long long": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 8
},
"unsigned short": {
"endian": "little",
"kind": "int",
"signed": false,
"size": 2
}
},
"enums": {},
"metadata": {
"format": "6.1.0",
"producer": {
"datetime": "2021-07-31T17:37:28.314801",
"name": "vmextract-by-hand",
"version": "0.0.1"
}
},
"symbols": {
"revision_id": {
"address": 0,
"constant_data": "MTU="
}
},
"user_types": {
"_VMCS": {
"fields": {
"ept": {
"offset": 320,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"executive_vmcs_ptr": {
"offset": 208,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr3": {
"offset": 736,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_cr4": {
"offset": 744,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"guest_pdpte": {
"offset": 928,
"type": {
"count": 4,
"kind": "array",
"subtype": {
"kind": "struct",
"name": "unsigned long long"
}
}
},
"guest_physical_addr": {
"offset": 328,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr3": {
"offset": 832,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"host_cr4": {
"offset": 840,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vmcs_link_ptr": {
"offset": 248,
"type": {
"kind": "struct",
"name": "unsigned long long"
}
},
"vpid": {
"offset": 220,
"type": {
"kind": "struct",
"name": "unsigned short"
}
}
},
"kind": "struct",
"size": 4096
}
}
}