mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-11 04:07:39 +02:00
Merge remote-tracking branch 'origin/816-port-cmdscan-and-console-plugins-from-vol2-to-vol3-please' into blackhat_2024
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
|
||||
@@ -22,6 +22,13 @@ import traceback
|
||||
from typing import Any, Dict, List, Tuple, Type, Union
|
||||
from urllib import parse, request
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
|
||||
HAS_ARGCOMPLETE = True
|
||||
except ImportError:
|
||||
HAS_ARGCOMPLETE = False
|
||||
|
||||
from volatility3.cli import text_filter
|
||||
import volatility3.plugins
|
||||
import volatility3.symbols
|
||||
@@ -351,6 +358,10 @@ class CommandLine:
|
||||
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
|
||||
|
||||
# Run the argparser
|
||||
if HAS_ARGCOMPLETE:
|
||||
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
|
||||
# before all the plugins have been added
|
||||
argcomplete.autocomplete(parser)
|
||||
args = parser.parse_args()
|
||||
if args.plugin is None:
|
||||
parser.error("Please select a plugin to run")
|
||||
|
||||
@@ -21,8 +21,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
# We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__
|
||||
self.choices = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -100,3 +98,20 @@ class HelpfulArgParser(argparse.ArgumentParser):
|
||||
|
||||
# return the number of arguments matched
|
||||
return len(match.group(1))
|
||||
|
||||
def _check_value(self, action: argparse.Action, value: Any) -> None:
|
||||
"""This is called to ensure a value is correct/valid
|
||||
|
||||
In normal operation, it would check that a value provided is valid and return None
|
||||
If it was not valid, it would throw an ArgumentError
|
||||
|
||||
When people provide a partial plugin name, we want to look for a matching plugin name
|
||||
which happens in the HelpfulSubparserAction's __call_method
|
||||
|
||||
To get there without tripping the check_value failure, we have to prevent the exception
|
||||
being thrown when the value is a HelpfulSubparserAction. This therefore affects no other
|
||||
checks for normal parameters.
|
||||
"""
|
||||
if not isinstance(action, HelpfulSubparserAction):
|
||||
super()._check_value(action, value)
|
||||
return None
|
||||
|
||||
@@ -21,6 +21,14 @@ from volatility3.framework import (
|
||||
plugins,
|
||||
)
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
|
||||
HAS_ARGCOMPLETE = True
|
||||
except ImportError:
|
||||
HAS_ARGCOMPLETE = False
|
||||
|
||||
|
||||
# Make sure we log everything
|
||||
|
||||
rootlog = logging.getLogger()
|
||||
@@ -276,6 +284,10 @@ class VolShell(cli.CommandLine):
|
||||
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
|
||||
|
||||
# Run the argparser
|
||||
if HAS_ARGCOMPLETE:
|
||||
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
|
||||
# before all the plugins have been added
|
||||
argcomplete.autocomplete(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
vollog.log(
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import datetime, logging, string
|
||||
|
||||
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.plugins.windows import (
|
||||
handles,
|
||||
info,
|
||||
pslist,
|
||||
psscan,
|
||||
sessions,
|
||||
thrdscan,
|
||||
)
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PsXView(plugins.PluginInterface):
|
||||
"""Lists all processes found via four of the methods described in \"The Art of Memory Forensics,\" which may help
|
||||
identify processes that are trying to hide themselves. I recommend using -r pretty if you are looking at this
|
||||
plugin's output in a terminal."""
|
||||
|
||||
# I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the funcitonality
|
||||
# which the original plugin used to do it.
|
||||
|
||||
# The sessions method is omitted because it begins with the list of processes found by Pslist anyway.
|
||||
|
||||
# Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the
|
||||
# code I do have from it, and will happily share it if anyone else wants to add it.
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 0)
|
||||
|
||||
valid_proc_name_chars = set(
|
||||
string.ascii_lowercase + string.ascii_uppercase + "." + " "
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Windows kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="info", component=info.Info, version=(1, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="pslist", component=pslist.PsList, version=(2, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="psscan", component=psscan.PsScan, version=(1, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="thrdscan", component=thrdscan.ThrdScan, version=(1, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="handles", component=handles.Handles, version=(1, 0, 0)
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="physical-offsets",
|
||||
description="List processes with physical offsets instead of virtual offsets.",
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _proc_name_to_string(self, proc):
|
||||
return proc.ImageFileName.cast(
|
||||
"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 _filter_garbage_procs(self, proc_list):
|
||||
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):
|
||||
if not self.config["physical-offsets"]:
|
||||
return offset
|
||||
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
layer_name = kernel.layer_name
|
||||
|
||||
try:
|
||||
_original_offset, _original_length, offset, _length, _layer_name = list(
|
||||
self.context.layers[layer_name].mapping(offset=offset, length=0)
|
||||
)[0]
|
||||
except exceptions.PagedInvalidAddressException:
|
||||
vollog.debug(f"Page fault: unable to translate {offset:0x}")
|
||||
|
||||
return offset
|
||||
|
||||
def _proc_list_to_dict(self, tasks):
|
||||
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):
|
||||
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):
|
||||
ret = []
|
||||
|
||||
for ethread in thrdscan.ThrdScan.scan_threads(
|
||||
self.context, module_name="kernel"
|
||||
):
|
||||
process = None
|
||||
try:
|
||||
process = ethread.owning_process()
|
||||
if not process.is_valid():
|
||||
continue
|
||||
|
||||
ret.append(process)
|
||||
except AttributeError:
|
||||
vollog.log(
|
||||
constants.LOGLEVEL_VVV,
|
||||
"Unable to find the owning process of ethread",
|
||||
)
|
||||
|
||||
return self._proc_list_to_dict(ret)
|
||||
|
||||
def _check_csrss_handles(self, tasks, layer_name, symbol_table):
|
||||
ret = []
|
||||
|
||||
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"))
|
||||
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.log(
|
||||
constants.LOGLEVEL_VVV, "Cannot access eprocess object table"
|
||||
)
|
||||
|
||||
return self._proc_list_to_dict(ret)
|
||||
|
||||
def _generator(self):
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
|
||||
layer_name = kernel.layer_name
|
||||
symbol_table = kernel.symbol_table_name
|
||||
|
||||
kdbg_list_processes = list(
|
||||
pslist.PsList.list_processes(
|
||||
context=self.context, layer_name=layer_name, symbol_table=symbol_table
|
||||
)
|
||||
)
|
||||
|
||||
# get processes from each source
|
||||
processes = {}
|
||||
|
||||
processes["pslist"] = self._check_pslist(kdbg_list_processes)
|
||||
processes["psscan"] = self._check_psscan(layer_name, symbol_table)
|
||||
processes["thrdscan"] = self._check_thrdscan()
|
||||
processes["csrss"] = self._check_csrss_handles(
|
||||
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)
|
||||
|
||||
for offset in offsets:
|
||||
proc = None
|
||||
|
||||
in_sources = {src: False for src in processes}
|
||||
|
||||
for source in processes:
|
||||
if offset in processes[source]:
|
||||
in_sources[source] = True
|
||||
if not proc:
|
||||
proc = processes[source][offset]
|
||||
|
||||
pid = proc.UniqueProcessId
|
||||
name = self._proc_name_to_string(proc)
|
||||
|
||||
exit_time = proc.get_exit_time()
|
||||
if type(exit_time) != datetime.datetime:
|
||||
exit_time = ""
|
||||
else:
|
||||
exit_time = str(exit_time)
|
||||
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(offset),
|
||||
name,
|
||||
pid,
|
||||
in_sources["pslist"],
|
||||
in_sources["psscan"],
|
||||
in_sources["thrdscan"],
|
||||
in_sources["csrss"],
|
||||
exit_time,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)"
|
||||
offset_str = "Offset" + offset_type
|
||||
|
||||
return TreeGrid(
|
||||
[
|
||||
(offset_str, format_hints.Hex),
|
||||
("Name", str),
|
||||
("PID", int),
|
||||
("pslist", bool),
|
||||
("psscan", bool),
|
||||
("thrdscan", bool),
|
||||
("csrss", bool),
|
||||
("Exit Time", str),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
@@ -29,12 +29,18 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
|
||||
self.set_type_class("files_struct", extensions.files_struct)
|
||||
self.set_type_class("kobject", extensions.kobject)
|
||||
self.set_type_class("cred", extensions.cred)
|
||||
self.set_type_class("inode", extensions.inode)
|
||||
# Might not exist in the current symbols
|
||||
self.optional_set_type_class("module", extensions.module)
|
||||
self.optional_set_type_class("bpf_prog", extensions.bpf_prog)
|
||||
self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct)
|
||||
self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t)
|
||||
|
||||
# kernels >= 4.18
|
||||
self.optional_set_type_class("timespec64", extensions.timespec64)
|
||||
# kernels < 4.18. Reuses timespec64 obj extension, since both has the same members
|
||||
self.optional_set_type_class("timespec", extensions.timespec64)
|
||||
|
||||
# Mount
|
||||
self.set_type_class("vfsmount", extensions.vfsmount)
|
||||
# Might not exist in older kernels or the current symbols
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
|
||||
import collections.abc
|
||||
import logging
|
||||
import stat
|
||||
from datetime import datetime
|
||||
import socket as socket_module
|
||||
from typing import Generator, Iterable, Iterator, Optional, Tuple, List
|
||||
from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Union
|
||||
|
||||
from volatility3.framework import constants, exceptions, objects, interfaces, symbols
|
||||
from volatility3.framework.renderers import conversion
|
||||
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
|
||||
@@ -1761,3 +1764,136 @@ class kernel_cap_t(kernel_cap_struct):
|
||||
)
|
||||
|
||||
return cap_value & self.get_kernel_cap_full()
|
||||
|
||||
|
||||
class timespec64(objects.StructType):
|
||||
def to_datetime(self) -> datetime:
|
||||
"""Returns the respective aware datetime"""
|
||||
|
||||
dt = conversion.unixtime_to_datetime(self.tv_sec + self.tv_nsec / 1e9)
|
||||
return dt
|
||||
|
||||
|
||||
class inode(objects.StructType):
|
||||
def is_valid(self) -> bool:
|
||||
# i_count is a 'signed' counter (atomic_t). Smear, or essentially a wrong inode
|
||||
# pointer, will easily cause an integer overflow here.
|
||||
return self.i_ino > 0 and self.i_count.counter >= 0
|
||||
|
||||
@property
|
||||
def is_dir(self) -> bool:
|
||||
"""Returns True if the inode is a directory"""
|
||||
return stat.S_ISDIR(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_reg(self) -> bool:
|
||||
"""Returns True if the inode is a regular file"""
|
||||
return stat.S_ISREG(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_link(self) -> bool:
|
||||
"""Returns True if the inode is a symlink"""
|
||||
return stat.S_ISLNK(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_fifo(self) -> bool:
|
||||
"""Returns True if the inode is a FIFO"""
|
||||
return stat.S_ISFIFO(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_sock(self) -> bool:
|
||||
"""Returns True if the inode is a socket"""
|
||||
return stat.S_ISSOCK(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_block(self) -> bool:
|
||||
"""Returns True if the inode is a block device"""
|
||||
return stat.S_ISBLK(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_char(self) -> bool:
|
||||
"""Returns True if the inode is a char device"""
|
||||
return stat.S_ISCHR(self.i_mode) != 0
|
||||
|
||||
@property
|
||||
def is_sticky(self) -> bool:
|
||||
"""Returns True if the sticky bit is set"""
|
||||
return (self.i_mode & stat.S_ISVTX) != 0
|
||||
|
||||
def get_inode_type(self) -> Union[str, None]:
|
||||
"""Returns inode type name
|
||||
|
||||
Returns:
|
||||
The inode type name
|
||||
"""
|
||||
if self.is_dir:
|
||||
return "DIR"
|
||||
elif self.is_reg:
|
||||
return "REG"
|
||||
elif self.is_link:
|
||||
return "LNK"
|
||||
elif self.is_fifo:
|
||||
return "FIFO"
|
||||
elif self.is_sock:
|
||||
return "SOCK"
|
||||
elif self.is_char:
|
||||
return "CHR"
|
||||
elif self.is_block:
|
||||
return "BLK"
|
||||
else:
|
||||
return None
|
||||
|
||||
def _time_member_to_datetime(self, member) -> datetime:
|
||||
if self.has_member(f"{member}_sec") and self.has_member(f"{member}_nsec"):
|
||||
# kernels >= 6.11 it's i_*_sec -> time64_t and i_*_nsec -> u32
|
||||
# Ref Linux commit 3aa63a569c64e708df547a8913c84e64a06e7853
|
||||
return conversion.unixtime_to_datetime(
|
||||
self.member(f"{member}_sec") + self.has_member(f"{member}_nsec") / 1e9
|
||||
)
|
||||
elif self.has_member(f"__{member}"):
|
||||
# 6.6 <= kernels < 6.11 it's a timespec64
|
||||
# Ref Linux commit 13bc24457850583a2e7203ded05b7209ab4bc5ef / 12cd44023651666bd44baa36a5c999698890debb
|
||||
return self.member(f"__{member}").to_datetime()
|
||||
elif self.has_member(member):
|
||||
# In kernels < 6.6 it's a timespec64 or timespec
|
||||
return self.member(member).to_datetime()
|
||||
else:
|
||||
raise exceptions.VolatilityException(
|
||||
"Unsupported kernel inode type implementation"
|
||||
)
|
||||
|
||||
def get_access_time(self) -> datetime:
|
||||
"""Returns the inode's last access time
|
||||
This is updated when inode contents are read
|
||||
|
||||
Returns:
|
||||
A datetime with the inode's last access time
|
||||
"""
|
||||
return self._time_member_to_datetime("i_atime")
|
||||
|
||||
def get_modification_time(self) -> datetime:
|
||||
"""Returns the inode's last modification time
|
||||
This is updated when the inode contents change
|
||||
|
||||
Returns:
|
||||
A datetime with the inode's last data modification time
|
||||
"""
|
||||
|
||||
return self._time_member_to_datetime("i_mtime")
|
||||
|
||||
def get_change_time(self) -> datetime:
|
||||
"""Returns the inode's last change time
|
||||
This is updated when the inode metadata changes
|
||||
|
||||
Returns:
|
||||
A datetime with the inode's last change time
|
||||
"""
|
||||
return self._time_member_to_datetime("i_ctime")
|
||||
|
||||
def get_file_mode(self) -> str:
|
||||
"""Returns the inode's file mode as string of the form '-rwxrwxrwx'.
|
||||
|
||||
Returns:
|
||||
The inode's file mode string
|
||||
"""
|
||||
return stat.filemode(self.i_mode)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
|
||||
Reference in New Issue
Block a user