Saturday, May 2, 2026

Volatility Volshell & Custom Plugins Beginner's Guide

Memory forensics gets a mention, from time to time in DFIR, sometimes feels like filesystem artifacts get more of the spotlight.  Also seems like resources for memory forensics are "here is a cheatsheet of Volatility commands...figure out the rest". 

This guide is meant to be a practical walkthrough of volshell for interactive artifact hunting, followed by building your own Volatility plugin in Python — specifically aimed at finding what traces of artifacts left in process memory.


Why Memory Forensics ?

Lets take it from the perspective of messaging apps and how it can help with forensics. Signal encrypts its local database and holds the key in memory. Telegram Desktop encrypts its cache. Many apps using the Electron framework (Discord, Slack, WhatsApp Desktop) keep parsed message objects in a JavaScript heap that never touches disk in plaintext form.

Memory changes the equation. When an app is running, the decrypted content exists in process memory — by definition, because the CPU has to be able to read it. A memory acquisition at the right moment captures:

  • Decrypted message content that is encrypted on disk
  • Recipient and sender identifiers, conversation IDs, and timestamps
  • Cached content (images, files) that may have already been deleted from disk
  • Session keys and tokens that can unlock further analysis
  • Artifacts from apps that store nothing locally at all (browser-based apps, certain mobile app classes)

The tradeoff is volatility itself: the window to capture this data closes when the process exits or the machine powers off. Below is a step-by-step setup for how to use volatility and 

Setup: Volatility 3 and a Memory Image

This guide uses Volatility 3 (not the legacy Volatility 2). The APIs, plugin structure, and volshell interface differ meaningfully between versions — if you find older tutorials online referencing vol.py with profile flags like --profile=Win10x64, they are Volatility 2 guides.

Install Volatility 3:

git clone https://github.com/volatilityfoundation/volatility3.git
cd volatility3
pip install -e .
pip install pefile yara-python

You'll need a memory image to work with. For practice, the MemLabs CTF challenge images are publicly available and cover real Windows memory scenarios. For lab work, tools like WinPmem (Windows), LiME (Linux), and osxpmem (macOS) produce acquisition images from live systems.

Verify your install with a basic plugin run against your image:

python vol.py -f memory.dmp windows.pslist

If you get a process list, you're ready. Volatility 3 auto-detects the OS and architecture from the image — no profile flag required.


Volshell: Interactive Memory Exploration

Volatility's standard plugins are useful for known artifacts. Volshell is where you go when you don't yet know what you're looking for, or when you need to poke at memory interactively before writing a formal plugin. Think of it as a Python REPL with the memory image loaded and Volatility's full API available.

Launch volshell against a Windows image:

python vol.py -f memory.dmp windows.volshell

You'll land in an interactive Python session. The context object self gives you access to the loaded image. A set of shortcut functions is pre-loaded to make navigation faster.

Core Volshell Commands

Command What it does
ps() List running processes (PID, PPID, name, offset)
cc(pid=<PID>) Change context — set the current process for memory reads
proc() Return the current process object
space() Return the current process virtual address space
db(addr, count=128) Display bytes at address (hex + ASCII)
dd(addr) Display doublewords (32-bit values)
dq(addr) Display quadwords (64-bit values)
dt("_EPROCESS", addr) Display a typed structure at an address

Finding a Target Process

Start by listing processes and finding your messaging app. Here we're looking for Discord, which runs as an Electron app:

>>> ps()
PID    PPID   Name             Offset
4      0      System           0xfa80...
...
3412   3388   Discord.exe      0xfa80...
3456   3412   Discord.exe      0xfa80...   <-- renderer process
3498   3412   Discord.exe      0xfa80...   <-- GPU process

Electron apps spawn multiple processes. The renderer process is where message content lives — it's the Chromium renderer that executes the JavaScript and holds the parsed message objects in its V8 heap. The renderer is typically not the first Discord.exe in the list; it will have the main process as its parent.

Switch context to the renderer process:

>>> cc(pid=3456)
Current context: Discord.exe @ 0xfa80..., pid=3456

Scanning Process Memory for Strings

The most direct approach for Electron apps is scanning process memory for recognizable string patterns. Discord message payloads follow a predictable JSON structure — "content", "channel_id", "author" — are good anchor strings.

In volshell, you can scan the process address space manually:

>>> # Get the address space for the current process
>>> addr_space = space()

>>> # Search for a pattern across all mapped regions
>>> search_bytes = b'"content"'
>>> for region in addr_space.get_valid_layers():
...     pass  # use the scanner approach below instead

Volatility 3 provides a scanner framework. The cleaner approach is to use BytesScanner from within volshell:

>>> from volatility3.framework.layers import scanner
>>> from volatility3.framework import interfaces

>>> # Scan process memory for "channel_id" (Discord message JSON key)
>>> target = b'"channel_id"'
>>> layer = space()
>>> hits = list(layer.scan(
...     context=self.context,
...     scanner=scanner.BytesScanner(needle=target)
... ))
>>> print(f"Found {len(hits)} hits")
Found 47 hits

>>> # Examine the region around the first hit
>>> db(hits[0], count=256)

The db() output will show you raw bytes with an ASCII representation. You're looking for readable JSON surrounding your anchor string — message content, usernames, timestamps.

Reading a Memory Region as Text

Once you have a hit address, you can extract a larger chunk and parse it as text:

>>> hit_addr = hits[0]
>>> # Read 2KB starting 512 bytes before the hit (catch leading JSON)
>>> raw = space().read(hit_addr - 512, 2048)
>>> print(raw.decode('utf-8', errors='replace'))

You'll get messy output mixed with non-printable bytes, but readable JSON fragments will surface. For Electron apps, look for complete JSON objects bracketed by { and } — these often contain entire message payloads with author information intact.


App-Specific Memory Artifacts: What to Hunt

Different apps leave different artifact patterns in memory. Here's where to focus for common third-party messaging apps.

Discord (Electron / V8 Heap)

Discord's desktop client is built on Electron. Message data is stored in the Chromium renderer's V8 JavaScript heap as serialized objects. Useful search strings:

  • "channel_id" — anchors message JSON objects
  • "author":{ — precedes author username and discriminator
  • "timestamp" — ISO 8601 timestamp tied to each message
  • discord.com/api/v — API endpoint strings indicating active request context

Discord's authentication token is a high-value artifact. Search for Authorization: (note the trailing space) in the renderer process — this surfaces the bearer token used for API calls, which can be used to retrieve message history via the API if the account is still active.

Slack (Electron / V8 Heap)

Slack Desktop is also Electron-based, with similar V8 heap structure. Key search patterns:

  • "client_msg_id" — unique message identifier in Slack's internal API format
  • "team_id" — workspace identifier
  • xoxs- or xoxb- — Slack token prefixes; finding one in memory gives you API access to the workspace
  • .slack.com/api/ — API call strings with endpoint paths

Signal Desktop (Electron + Encrypted SQLCipher DB)

Signal Desktop is more complex. The local database is encrypted with SQLCipher, but the encryption key is loaded into the renderer process memory at runtime. Search patterns:

  • "body": — message body field in Signal's internal message format
  • "conversationId" — links messages to contacts or groups
  • "key":" followed by 64 hex characters — the SQLCipher database key (if you find this, you can decrypt the on-disk database)
  • +1 followed by 10 digits — phone numbers stored in contact records

The SQLCipher key is the crown jewel for Signal analysis. Once extracted from memory, it can be passed to SQLCipher-aware tools to decrypt the on-disk database at %AppData%\Signal\sql\db.sqlite, giving you the complete message history.

Telegram Desktop (Qt / Custom Storage)

Telegram Desktop is written in C++ using Qt — not Electron. Memory artifacts are structured differently. Look for:

  • UTF-16 encoded message strings (common in Qt's QString — you'll see null bytes between each character)
  • t.me/ strings — channel and user deep links
  • Phone number patterns in the user cache region of memory

For UTF-16 scanning in volshell, encode your search term accordingly:

>>> # Search for UTF-16 LE encoded string (Qt QString format)
>>> target_utf16 = "Telegram".encode('utf-16-le')
>>> hits = list(space().scan(
...     context=self.context,
...     scanner=scanner.BytesScanner(needle=target_utf16)
... ))

Writing a Custom Volatility 3 Plugin in Python

Volshell is for interactive exploration. Once you know what you're looking for, a plugin turns that exploration into a repeatable, shareable tool you can run across multiple images.

Plugin Architecture: The Essentials

Every Volatility 3 plugin is a Python class that inherits from PluginInterface. Three things are required:

  1. _required_framework_version — the minimum Volatility 3 framework version
  2. get_requirements() — declares what the plugin needs (OS, layer, symbol tables)
  3. run() — the entry point that returns a TreeGrid of results

Complete Example: Discord Message Extractor

Here is a complete, working Volatility 3 plugin that scans for Discord processes, searches their memory for message JSON fragments, and outputs readable results. Save this as volatility3/plugins/windows/discord_messages.py:

import logging
import re
from typing import List, Iterator, Tuple

from volatility3.framework import interfaces, renderers, constants
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanner
from volatility3.plugins.windows import pslist

vollog = logging.getLogger(__name__)


class DiscordMessages(interfaces.plugins.PluginInterface):
    """Extract Discord message fragments from process memory."""

    _required_framework_version = (2, 0, 0)
    _version = (1, 0, 0)

    @classmethod
    def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
        return [
            requirements.ModuleRequirement(
                name="kernel",
                description="Windows kernel",
                architectures=["Intel32", "Intel64"],
            ),
            requirements.PluginRequirement(
                name="pslist",
                plugin=pslist.PsList,
                version=(2, 0, 0),
            ),
            requirements.IntRequirement(
                name="context_bytes",
                description="Bytes to capture around each hit",
                default=512,
                optional=True,
            ),
        ]

    def _get_discord_procs(self) -> Iterator:
        """Yield process objects for all Discord.exe instances."""
        kernel = self.context.modules[self.config["kernel"]]
        for proc in pslist.PsList.list_processes(
            context=self.context,
            layer_name=kernel.layer_name,
            symbol_table=kernel.symbol_table_name,
        ):
            try:
                name = proc.ImageFileName.cast(
                    "string",
                    max_length=proc.ImageFileName.vol.count,
                    errors="replace",
                ).lower()
                if "discord" in name:
                    yield proc
            except Exception:
                continue

    def _scan_process_memory(
        self, proc, needle: bytes, context_bytes: int
    ) -> Iterator[Tuple[int, bytes]]:
        """Scan a process's address space for needle, yielding (offset, surrounding_bytes)."""
        try:
            proc_layer_name = proc.add_process_layer()
            proc_layer = self.context.layers[proc_layer_name]
        except Exception as e:
            vollog.debug(f"Could not create process layer: {e}")
            return

        for hit in proc_layer.scan(
            context=self.context,
            scanner=scanner.BytesScanner(needle=needle),
        ):
            try:
                start = max(0, hit - context_bytes)
                raw = proc_layer.read(start, context_bytes * 2, pad=True)
                yield hit, raw
            except Exception:
                continue

    def _extract_json_fragment(self, raw: bytes) -> str:
        """Pull the most readable JSON fragment from raw bytes."""
        # Decode with replacement for non-UTF8 bytes
        text = raw.decode("utf-8", errors="replace")
        # Find content between first { and last } in the window
        match = re.search(r'\{[^{}]{20,}\}', text)
        if match:
            return match.group(0)[:400]  # cap output length
        # Fall back to printable ASCII run
        printable = re.sub(r'[^\x20-\x7e]', ' ', text)
        return printable[:400].strip()

    def run(self) -> interfaces.renderers.TreeGrid:
        context_bytes = self.config.get("context_bytes", 512)

        # Discord message JSON keys to search for
        search_terms = [
            b'"channel_id"',
            b'"content"',
            b'"author":{',
        ]

        rows = []
        seen_offsets = set()

        for proc in self._get_discord_procs():
            pid = int(proc.UniqueProcessId)
            ppid = int(proc.InheritedFromUniqueProcessId)

            for needle in search_terms:
                for offset, raw in self._scan_process_memory(
                    proc, needle, context_bytes
                ):
                    if offset in seen_offsets:
                        continue
                    seen_offsets.add(offset)

                    fragment = self._extract_json_fragment(raw)
                    rows.append((0, (
                        pid,
                        ppid,
                        needle.decode("utf-8"),
                        hex(offset),
                        fragment,
                    )))

        return renderers.TreeGrid(
            [
                ("PID", int),
                ("PPID", int),
                ("Matched Key", str),
                ("Offset", str),
                ("Fragment", str),
            ],
            iter(rows),
        )

Run the plugin:

python vol.py -f memory.dmp windows.discord_messages
python vol.py -f memory.dmp windows.discord_messages --context-bytes 1024

The output will be a table of hits: the PID, which JSON key triggered the hit, the memory offset, and the surrounding JSON fragment. Post-process the Fragment column with a simple Python script to extract clean message content.

Adapting the Plugin for Other Apps

To target a different app, change two things:

  1. The process name filter in _get_discord_procs() — rename the method and update the string match (e.g., "slack", "signal", "telegram")
  2. The search terms list — replace with the anchor strings you identified in volshell exploration for the target app

For UTF-16 targets like Telegram Desktop, encode your search terms as b'\x74\x00\x2e\x00\x6d\x00\x65\x00' (the UTF-16 LE encoding of t.me) and update your fragment extractor to handle UTF-16 decoding:

text = raw.decode("utf-16-le", errors="replace")

Practical Workflow: From Acquisition to Artifacts

Putting it together into an end-to-end workflow for a messaging app investigation:

  1. Acquire memory while the target app is running. Use WinPmem or a hardware imager. Time matters — acquiring within minutes of the event window is significantly better than hours later for cached message content.
  2. Run pslist first (windows.pslist) to confirm the target process was running at acquisition time and note all PIDs.
  3. Open volshell and switch context to the target PID. Run a quick string scan with a few candidate anchor terms. Inspect hits with db() to confirm you're finding the right structure.
  4. Refine your search terms based on what you found interactively. Note which terms produce clean hits and which produce noise.
  5. Build or adapt a plugin using the pattern above. Run it against the image and export output for review.
  6. Cross-reference with disk artifacts — SQLite databases, log files, registry entries — to corroborate and extend the memory findings.

Common Pitfalls for Beginners

  • Scanning the wrong process. Electron apps spawn 5–10 processes. The main process rarely holds message data — the renderer process does. Use ppid relationships to identify the renderer and switch context there.
  • Assuming ASCII encoding. Qt applications (Telegram, some others) use UTF-16. If your string scan returns zero hits and you're certain the app was active, try encoding your search term as UTF-16 LE.
  • Reading too small a context window. The default 128 bytes from db() will often only show you part of a message object. Read at least 512–1024 bytes to capture full JSON structures.
  • Not accounting for garbage-collected memory. The V8 JavaScript heap is garbage-collected. Message objects may have been collected if the conversation was scrolled out of view. Focus on recently active conversations for the best hit rate.
  • Missing paged-out memory. Volatility will pad unreadable pages. Zero-filled runs in your output (00 00 00 00) indicate paged-out content that wasn't present in physical memory at acquisition time — this is expected and not a tool error.

Where to Go Next

  • Volatility 3 documentation — the official ReadTheDocs site covers the full plugin API and framework internals
  • MemLabs CTFgithub.com/stuxnet999/MemLabs — free memory images with progressively complex challenges
  • forensafe memory series — covers certificate extraction and other artifact classes in depth
  • YARA integration — Volatility 3 supports YARA rule scanning via windows.vadyarascan; once you've characterized an artifact pattern in volshell, encoding it as a YARA rule lets you scan memory images at scale

This post is part of the ForensicFellowship blog.  Feedback and corrections welcome.

Recent Posts

DFIR Watchtower — Blog Post: Emerging Third-Party App Forensic Artifacts

Every week the artifact landscape shifts. New apps get deployed across enterprise networks, threat actors adapt their tooling, and the forensic community races to document what gets left behind. The end of April Watchtower edition has several research threads worth tracking closely, from VPN client artifacts to articles about memory forensics and Windows registry keys. 

Here's the practitioner-focused breakdown on a few highlighted articles:


Tailscale: Four-Part Artifact Series 

The week's most significant forensic research came from ogmini, who published a four-part deep dive into Tailscale artifacts across platforms. Tailscale has moved from developer novelty to enterprise standard — it's now a realistic presence on corporate endpoints, cloud instances, and even some mobile devices. If you're investigating an endpoint and haven't started accounting for Tailscale, you're behind.

The series covers:

  • Part 1 — Initial installation footprint, registry keys created, and service artifacts on Windows
  • Part 2 — Log file locations, connection state persistence, and peer relationship data stored locally
  • Part 3 — Network configuration artifacts and how Tailscale's WireGuard layer surfaces (or doesn't) in standard packet capture workflows
  • Part 4 — Cross-platform differences and what macOS and Linux leave behind compared to Windows

A few things stand out operationally. Tailscale stores peer node data locally, which means even without network visibility you can reconstruct which machines a compromised endpoint was connected to at investigation time. The log rotation behavior differs by OS, so your collection window matters. On Windows, the MagicDNS configuration artifacts can give you hostnames of internal nodes that an attacker was communicating with — useful if you're mapping lateral movement through a Tailscale mesh.

This is the kind of artifact series worth pulling into your triage checklist now, before you encounter it mid-investigation.

References: Part 1 · Part 2 · Part 3 · Part 4


Bissa Scanner: Documenting a Threat Actor's Tooling Artifacts

The DFIR Report published analysis of Bissa Scanner, an AI-assisted mass exploitation and credential harvesting tool observed in the wild. Beyond the threat intelligence value, there's direct artifact value here for practitioners: when you know what a tool does, you know what it leaves behind.

Bissa Scanner's operational pattern involves scanning at scale for vulnerable endpoints, then executing credential harvesting post-exploitation. The DFIR Report's analysis documents the file system artifacts, process execution chains, and network indicators the tool generates. If you're doing triage on an externally-facing system that was potentially compromised in the past 30–60 days, the indicators from this report are worth adding to your initial keyword and hash checks.

The AI-assisted aspect is notable not because the forensics change dramatically, but because the speed of operation compresses your detection window. Artifacts from automated tooling can accumulate faster than a human operator would produce them — timestamp clustering in prefetch, event logs, and filesystem MFT entries can be a tell.

Reference: Bissa Scanner Exposed — The DFIR Report


Memory Forensics: Certificates as an Artifact Class

forensafe's ongoing memory forensics series this week covered memory certificates — specifically how certificate objects loaded into process memory can be identified and extracted during volatile memory analysis. This is an underutilized artifact class.

When a process performs TLS operations, the certificate chain gets loaded into memory. That means even if an attacker has cleaned up disk artifacts, if you captured memory at the right time, you can recover the certificates associated with C2 communications — including self-signed certs with embedded metadata that may not appear anywhere in network logs if traffic was captured post-decryption.

Volatility's windows.cmdline and windows.malfind are the usual starting points for process-level investigation, but the certificate extraction path requires a different approach — targeting the certificate store objects in memory rather than code regions. The forensafe post walks through the specific structures to target.

Pair this with volshell work covered in righteousit's post this week on Fun With volshell — if you're doing custom memory analysis and haven't gotten comfortable with volshell for interactive exploration, it's worth the investment. The ability to interactively query memory structures without writing a full plugin accelerates artifact class discovery significantly.

References: Memory Certificates — forensafe · Fun With volshell — righteousit


Wi-Fi Events as Location Artifacts

Berla published a post this week on identifying locations of interest using Wi-Fi events — specifically how Wi-Fi probe requests, connection logs, and BSSID records stored in vehicle infotainment systems and mobile devices can be used to reconstruct location history without GPS data.

This is a useful addition to the location artifact toolkit for cases where GPS data is absent, disabled, or tampered with. Wi-Fi probe requests are largely passive and logged automatically, making them harder for a target to suppress without disabling wireless entirely. BSSID records can be reverse-geocoded against commercial and open databases (Wigle, etc.) to produce location estimates with varying precision depending on AP density.

The vehicle forensics angle from Berla is worth noting separately: infotainment systems retain Wi-Fi connection histories that most investigators don't prioritize, and these can corroborate or contradict other timeline evidence.

Reference: Identifying Locations of Interest Using Wi-Fi Events — Berla


What to Add to Your Triage Checklist

Based on this week's research, here's what's worth integrating into active workflows:

  • Tailscale — Add log locations and peer node artifact paths to your collection scripts for Windows, macOS, and Linux. Start accounting for it in IR scoping questions.
  • Memory certificates — If you're capturing volatile memory on suspected C2-connected endpoints, add certificate extraction to your analysis workflow.
  • Wi-Fi BSSID records — Add to location corroboration methodology for cases where GPS is unavailable.

This post is derived from the DFIR Watchtower weekly digest — an automated intelligence aggregator tracking 17 DFIR sources. All linked research is credited to its original authors.

Recent Posts