Enrichment layer 1: correlating VAD findings with Sysmon process creation history
Finding an anonymous executable region in a process is a strong indicator, but it answers only part of the question. You know something is in the memory of process X. You do not yet know how it got there, when it appeared, or what chain of events preceded it. Windows event logs and Sysmon telemetry fill that gap, giving you a timeline of everything that happened to that process before and after the injection.
The first enrichment step is to take the PID and process name from your Velociraptor findings and pull the full process creation chain from Sysmon Event ID 1. This tells you what created the process you are investigating, what command line was used, and how far back the process tree goes. An injected svchost.exe that was spawned by services.exe with normal arguments is a very different finding from an injected svchost.exe that was spawned by a PowerShell process with an encoded command argument.
-- VQL: Enrich suspicious process with its creation history from Sysmon EventLog
-- Replace TargetPid with the PID from your injection hunt results
LET target_pid = 4892 -- replace with suspicious PID from hunt results
-- Step 1: Get the full ancestry chain for the suspicious process
SELECT
ProcessId AS PID,
ParentProcessId AS ParentPID,
Name AS ProcessName,
CommandLine,
User,
CreateTime,
Exe AS ExecutablePath,
-- Hash the executable to check reputation
hash(path=Exe, hashselect="SHA256") AS ExecutableHash
FROM pslist()
WHERE ProcessId = target_pid
OR ProcessId IN (
-- Also pull the parent and grandparent
SELECT ParentProcessId FROM pslist() WHERE ProcessId = target_pid
)
-- VQL: Pull Sysmon Event ID 1 records for process creation context
-- This gives you the full command line and parent details at process start time
-- even if the process has been running for hours
SELECT
System.TimeCreated.SystemTime AS EventTime,
EventData.ProcessId AS PID,
EventData.ParentProcessId AS ParentPID,
EventData.Image AS ProcessImage,
EventData.CommandLine AS CommandLine,
EventData.ParentImage AS ParentImage,
EventData.ParentCommandLine AS ParentCommandLine,
EventData.User AS RunningAsUser,
EventData.IntegrityLevel AS IntegrityLevel,
EventData.Hashes AS Hashes,
-- Current directory at time of launch is often revealing
EventData.CurrentDirectory AS WorkingDirectory
FROM watch_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 1
AND (
EventData.ProcessId = format(format="%d", args=target_pid)
OR EventData.ParentProcessId = format(format="%d", args=target_pid)
)
ORDER BY EventTime ASC
Pay careful attention to the IntegrityLevel field in the Sysmon Event ID 1 record. A process running at High or System integrity that was spawned from a process at Medium integrity indicates a privilege escalation occurred somewhere in the chain. A process running as SYSTEM that was spawned from a user-context process through an unusual parent (WmiPrvSE.exe, msdtc.exe, or similar) indicates lateral movement or exploitation of a privileged service.
The Hashes field in Sysmon Event ID 1 contains the SHA256 of the executable at the time it launched. Take that hash and check it against threat intelligence immediately. If the binary has been renamed to disguise itself, the hash will still identify the real malware family even if the filename looks legitimate.
Enrichment layer 2: memory image acquisition and Volatility cross-analysis
Velociraptor can acquire a full memory image from a live remote system and make it available for offline analysis with Volatility. This is the bridge between the live query capabilities of Velociraptor and the deep structural analysis capabilities of Volatility. Where Velociraptor gives you fleet-wide fast triage, Volatility gives you the forensic depth to answer definitively what the injected code is, what it has been doing, and what it has communicated with.
-- VQL: Acquire full memory image from the target machine
-- Run this as a client artefact against the specific host that showed injection
SELECT * FROM Artifact.Windows.Memory.Acquisition(
-- Output path on the target machine (temporary staging)
destination="C:/Windows/Temp/mem_acquisition.raw",
-- Also acquire the pagefile for complete coverage
also_upload_pagefile=TRUE
)
Once the acquisition completes, Velociraptor uploads the image to the server and you can download it for Volatility analysis. The acquisition typically takes 2-10 minutes depending on RAM size. Do not wait for it to complete before starting other enrichment steps the Velociraptor VAD queries already captured the volatile data you need for triage, so the memory image is for deep analysis that can run in parallel.
# Volatility 3 analysis workflow against the acquired image
# Replace memory.raw with your downloaded image filename
# Step 1: Confirm the image profile and get baseline info
vol -f memory.raw windows.info
# Step 2: Cross-reference the suspicious PID from Velociraptor
# Get the full process details including PPID chain
vol -f memory.raw windows.pstree | grep -A2 -B2 "4892"
# Step 3: Run malfind against the specific suspicious process
# malfind finds executable regions with no disk backing - same concept as our VQL
vol -f memory.raw windows.malfind --pid 4892
# Example malfind output for a Cobalt Strike injected process:
# Process: svchost.exe Pid: 4892 Address: 0x1d0000
# Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
# Hexdump:
# 4d 5a 90 00 03 00 00 00 MZ......
# This confirms the Velociraptor VAD finding with an independent second source
# Step 4: Dump the suspicious region for further analysis
vol -f memory.raw windows.dumpfiles --virtaddr 0x1d0000 --pid 4892 -o /tmp/dumped/
# Step 5: Analyse the dumped region
file /tmp/dumped/file.0x4892.0x1d0000.img
strings -a -n 8 /tmp/dumped/file.0x4892.0x1d0000.img | grep -iE "(http|https|192\.|10\.|beacon|sleep|checkin)"
sha256sum /tmp/dumped/file.0x4892.0x1d0000.img
# Step 6: Extract network connections from memory for C2 identification
vol -f memory.raw windows.netscan | grep "4892"
# Sample output:
# TCPv4 192.168.1.105 54321 198.51.100.45 443 ESTABLISHED 4892 svchost.exe
# This is the C2 connection - 198.51.100.45:443 is the attacker server
# Step 7: Check for credential material in the injected process
# If the injection target was lsass.exe or if the injected code accessed lsass
vol -f memory.raw windows.lsadump | head -50
# Step 8: Extract the full module list to find reflectively loaded components
vol -f memory.raw windows.dlllist --pid 4892
# Cross-reference against what Velociraptor reported in the modules() query
# Any discrepancy = something was loaded that the OS does not officially track
Enrichment layer 3: correlating with Windows event logs for timeline reconstruction
The memory analysis and Velociraptor VAD findings tell you what is in memory now. The Windows event logs tell you the story of how it got there. Building a complete timeline from event logs around the injection event turns an isolated finding into a full attack narrative that supports incident response decisions.
-- VQL: Pull all security-relevant events for the suspicious process
-- and the 30 minutes before and after it was created
-- This builds a timeline around the injection event
LET process_create_time = timestamp(string="2024-04-02T22:14:00Z") -- from Sysmon Event 1
LET window_start = process_create_time - 1800 -- 30 minutes before
LET window_end = process_create_time + 1800 -- 30 minutes after
SELECT
System.TimeCreated.SystemTime AS EventTime,
System.EventID.Value AS EventID,
-- Map event IDs to human-readable names
switch(
condition=System.EventID.Value = 4624, then="Logon Success",
condition=System.EventID.Value = 4625, then="Logon Failure",
condition=System.EventID.Value = 4648, then="Explicit Credentials Used",
condition=System.EventID.Value = 4688, then="Process Created",
condition=System.EventID.Value = 4689, then="Process Terminated",
condition=System.EventID.Value = 4698, then="Scheduled Task Created",
condition=System.EventID.Value = 7045, then="Service Installed",
condition=System.EventID.Value = 4720, then="User Account Created",
condition=System.EventID.Value = 4732, then="Member Added to Local Group",
else=format(format="Event %d", args=System.EventID.Value)
) AS EventName,
-- Pull the most relevant fields
get(item=EventData, field="SubjectUserName") AS Actor,
get(item=EventData, field="TargetUserName") AS Target,
get(item=EventData, field="ProcessName") AS ProcessName,
get(item=EventData, field="CommandLine") AS CommandLine,
get(item=EventData, field="IpAddress") AS SourceIP,
get(item=EventData, field="WorkstationName") AS SourceHost
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.TimeCreated.SystemTime >= window_start
AND System.TimeCreated.SystemTime <= window_end
AND System.EventID.Value IN (4624, 4625, 4648, 4688, 4689, 4698, 7045, 4720, 4732)
ORDER BY EventTime ASC
-- VQL: Sysmon timeline for the injection window
-- Covers network connections, file operations, registry changes, and pipe creation
-- All correlated to the same time window around the injection
SELECT
System.TimeCreated.SystemTime AS EventTime,
System.EventID.Value AS SysmonEventID,
switch(
condition=System.EventID.Value = 1, then="Process Created",
condition=System.EventID.Value = 3, then="Network Connection",
condition=System.EventID.Value = 5, then="Process Terminated",
condition=System.EventID.Value = 6, then="Driver Loaded",
condition=System.EventID.Value = 7, then="Image Loaded",
condition=System.EventID.Value = 8, then="CreateRemoteThread",
condition=System.EventID.Value = 10, then="ProcessAccess (LSASS/injection)",
condition=System.EventID.Value = 11, then="File Created",
condition=System.EventID.Value = 12, then="Registry Key Created/Deleted",
condition=System.EventID.Value = 13, then="Registry Value Set",
condition=System.EventID.Value = 17, then="Pipe Created",
condition=System.EventID.Value = 18, then="Pipe Connected",
condition=System.EventID.Value = 22, then="DNS Query",
condition=System.EventID.Value = 25, then="Process Tampering",
else=format(format="Sysmon %d", args=System.EventID.Value)
) AS EventType,
get(item=EventData, field="Image") AS ProcessImage,
get(item=EventData, field="TargetImage") AS TargetProcess,
get(item=EventData, field="DestinationIp") AS DestIP,
get(item=EventData, field="DestinationPort") AS DestPort,
get(item=EventData, field="QueryName") AS DNSQuery,
get(item=EventData, field="TargetFilename") AS FilePath,
get(item=EventData, field="TargetObject") AS RegistryKey,
get(item=EventData, field="PipeName") AS PipeName,
get(item=EventData, field="GrantedAccess") AS AccessMask
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.TimeCreated.SystemTime >= window_start
AND System.TimeCreated.SystemTime <= window_end
ORDER BY EventTime ASC
When you view this combined timeline, you are looking for the moment the injection happened and everything that preceded it. Work backwards from the injection event. What network connections were made immediately before? What processes were created? Was there a suspicious DNS query 30 seconds before the injected process started? Was there a logon event from an unusual source IP around the same time?
Common patterns you will see in real injection chains: a macro-enabled Office document spawning PowerShell (Event ID 1 with ParentImage=winword.exe and Image=powershell.exe), followed immediately by a DNS query for a domain that resolves to a known hosting provider (Event ID 22), followed by a network connection outbound on port 443 (Event ID 3), followed by a CreateRemoteThread event (Event ID 8) into a separate process. That four-event sequence, compressed into 10-30 seconds, is the macro-to-shellcode-to-injected-process chain in plain text.
Enrichment layer 4: filesystem artefacts Prefetch, Amcache, and MFT
After establishing what is in memory and correlating with event logs, the filesystem artefacts provide a third independent source of evidence that can confirm or add detail to the picture. Prefetch files tell you what ran and when, Amcache provides execution hashes for tools that have since been deleted, and the MFT timestamps tell you when files appeared and changed on disk.
-- VQL: Pull Prefetch execution history for processes related to the injection
-- Prefetch files survive even after the executable is deleted
SELECT
Name AS PrefetchFile,
-- Extract process name from prefetch filename (format: PROCESSNAME-HASH.pf)
regex_transform(
source=Name,
map={"^(.+)-[A-F0-9]{8}\\.pf$": "$1"}
) AS ExecutableName,
Mtime AS LastExecutionTime,
Ctime AS FirstSeen,
-- Read the prefetch binary for run count and timestamps
-- (basic metadata only without full parsing)
Size AS FileSize
FROM glob(
globs="C:/Windows/Prefetch/*.pf"
)
-- Filter to processes related to the injection investigation
WHERE ExecutableName IN (
"POWERSHELL",
"CMD",
"WSCRIPT",
"CSCRIPT",
"MSHTA",
"REGSVR32",
"RUNDLL32",
-- Add the specific process name from your injection findings
"SVCHOST" -- example
)
ORDER BY LastExecutionTime DESC
-- VQL: Amcache analysis for executed files with hash attribution
-- Amcache persists SHA1 hashes of files that have executed
-- even if those files have since been deleted
SELECT
BinProductVersion AS FileVersion,
BinFileDescription AS FileDescription,
FileId AS SHA1Hash,
LongPathHash AS FilePath,
LinkDate AS CompileTimestamp,
-- The write time on the Amcache key approximates first execution time
Modified AS ApproxFirstExecution
FROM read_reg_key(
globs="HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/AppCompatCache"
)
-- Alternatively, parse the Amcache.hve directly for richer data
-- VQL to invoke AmcacheParser via shell
SELECT * FROM execve(argv=[
"C:/tools/AmcacheParser.exe",
"-f", "C:/Windows/AppCompat/Programs/Amcache.hve",
"--csv", "C:/Windows/Temp/amcache_output",
"-q"
])
WHERE ReturnCode = 0
-- VQL: MFT timeline analysis around the injection window
-- The MFT records precise timestamps for every file creation, modification,
-- and access revealing dropped tools, staged payloads, and cleanup attempts
SELECT
FullPath AS FilePath,
Name AS FileName,
-- NTFS records four timestamps per file (MACB)
SI_Mtime AS Modified,
SI_Atime AS Accessed,
SI_Ctime AS MetadataChanged,
SI_Btime AS Created,
FileSize,
IsDir,
-- Flag executables and scripts specifically
if(condition=Name =~ "(?i)\\.(exe|dll|ps1|vbs|js|bat|cmd|hta|scr)$",
then="EXECUTABLE", else="DATA") AS FileType
FROM parse_mft(
-- Direct MFT access requires admin privileges
filename="C:/$MFT",
accessor="ntfs"
)
WHERE
-- Focus on the 2-hour window around the injection
SI_Btime >= window_start
AND SI_Btime <= window_end
-- Focus on likely staging and tool-drop locations
AND (
FullPath =~ "(?i)(\\Temp\\|\\AppData\\|\\ProgramData\\|\\Public\\)"
OR FullPath =~ "(?i)(\\Downloads\\|\\Desktop\\)"
)
AND NOT IsDir
ORDER BY SI_Btime ASC
The MFT timeline is particularly valuable for finding files that were created, used briefly, and deleted a pattern that is invisible in most log sources but leaves a permanent MFT record. Attackers who drop a tool, run it, and delete it often miss the fact that the MFT entry persists long after the file is gone. Cross-reference MFT creation timestamps with Prefetch execution timestamps for the same filename: if a file was created at 22:14:31 and Prefetch shows it ran at 22:14:35 with a single run count, it was a one-shot tool that was deleted after use. That four-second gap between file creation and first execution is the attacker staging and immediately running a tool.
Enrichment layer 5: network telemetry correlation
The injected process almost always makes network connections that is usually the whole point. Correlating the memory and event log findings with network telemetry from Zeek or your firewall gives you the C2 infrastructure details that complete the attack picture. You now know not just that a machine is compromised, but exactly where it is communicating with and what that communication looks like.
-- VQL: Pull all network connections made by the suspicious process
-- Cross-reference with Sysmon network events and live connections
-- Current network connections (live state)
SELECT
Pid,
Name AS ProcessName,
LocalAddress,
LocalPort,
RemoteAddress,
RemotePort,
Status AS ConnectionState,
-- Flag obviously suspicious destinations
if(condition=RemotePort IN (4444, 8080, 8443, 1337, 31337),
then="COMMON_C2_PORT", else="CHECK") AS PortFlag
FROM netstat()
WHERE Pid = target_pid
UNION
-- Historical connections from Sysmon Event ID 3
SELECT
get(item=EventData, field="ProcessId") AS Pid,
get(item=EventData, field="Image") AS ProcessName,
get(item=EventData, field="SourceIp") AS LocalAddress,
get(item=EventData, field="SourcePort") AS LocalPort,
get(item=EventData, field="DestinationIp") AS RemoteAddress,
get(item=EventData, field="DestinationPort") AS RemotePort,
"HISTORICAL" AS ConnectionState,
get(item=EventData, field="UtcTime") AS EventTime
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 3
AND get(item=EventData, field="ProcessId") = format(format="%d", args=target_pid)
ORDER BY EventTime DESC
-- VQL: DNS query history for the suspicious process
-- Sysmon Event ID 22 captures DNS queries with the requesting process
-- This gives you the domain names the injected code tried to resolve
SELECT
System.TimeCreated.SystemTime AS QueryTime,
get(item=EventData, field="QueryName") AS Domain,
get(item=EventData, field="QueryResults") AS ResolvedIPs,
get(item=EventData, field="Image") AS RequestingProcess,
-- Flag high-risk domain patterns
if(condition=get(item=EventData, field="QueryName") =~
"(?i)(\.tk$|\.xyz$|\.top$|bit\.ly|tinyurl|ngrok\.io|pastebin)",
then="SUSPICIOUS_TLD", else="CHECK") AS DomainFlag
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 22
AND get(item=EventData, field="Image") =~ "svchost" -- match process from findings
AND System.TimeCreated.SystemTime >= window_start
ORDER BY QueryTime ASC
# Zeek correlation: match the C2 IP from memory analysis against network logs
# Run this on your Zeek sensor after identifying the C2 IP from Volatility netscan
C2_IP="198.51.100.45"
WINDOW_START="2024-04-02T22:00:00"
WINDOW_END="2024-04-02T23:00:00"
# All connections to the C2 IP in the incident window
cat /opt/zeek/logs/current/conn.log | \
python3 -c "
import json, sys
from datetime import datetime
c2_ip = '$C2_IP'
for line in sys.stdin:
try:
rec = json.loads(line)
if rec.get('id.resp_h') == c2_ip or rec.get('id.orig_h') == c2_ip:
print(json.dumps({
'time': rec.get('ts'),
'src': rec.get('id.orig_h'),
'src_port': rec.get('id.orig_p'),
'dst': rec.get('id.resp_h'),
'dst_port': rec.get('id.resp_p'),
'duration': rec.get('duration'),
'bytes_out': rec.get('orig_bytes'),
'bytes_in': rec.get('resp_bytes'),
'protocol': rec.get('proto'),
'state': rec.get('conn_state')
}, indent=2))
except:
pass
"
# TLS session details if C2 uses HTTPS
cat /opt/zeek/logs/current/ssl.log | \
python3 -c "
import json, sys
for line in sys.stdin:
try:
rec = json.loads(line)
if rec.get('id.resp_h') == '$C2_IP':
print(f\"JA3: {rec.get('ja3','none')} | SNI: {rec.get('server_name','none')} | Cert: {rec.get('subject','none')}\")
except:
pass
"
The combination of the destination IP from netscan, the JA3 fingerprint from Zeek’s ssl.log, and the DNS query history from Sysmon Event ID 22 gives you a complete C2 profile. Submit the JA3 hash to threat intelligence platforms if it matches a known Cobalt Strike or Meterpreter default, that narrows the toolset significantly. Take the destination IP and check it against passive DNS to find other domains that have resolved to the same infrastructure. Those other domains may be used in other campaigns targeting your sector.
Enrichment layer 6: lateral movement indicators
A compromised machine is rarely the end of the story. Once an attacker has injected code and established C2, the next phase is almost always lateral movement. Looking for signs that the injected process was used as a pivot point or that the credentials it accessed were used elsewhere rounds out the investigation and determines scope.
-- VQL: Detect lateral movement from the compromised host
-- Look for authentication attempts to other systems originating from this host
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="TargetUserName") AS AccountUsed,
get(item=EventData, field="TargetServerName") AS TargetSystem,
get(item=EventData, field="LogonType") AS LogonType,
switch(
condition=get(item=EventData, field="LogonType") = "3",
then="Network (SMB/WMI/DCOM)",
condition=get(item=EventData, field="LogonType") = "10",
then="Remote Interactive (RDP)",
condition=get(item=EventData, field="LogonType") = "9",
then="NewCredentials (runas /netonly)",
else=format(format="Type %v", args=get(item=EventData, field="LogonType"))
) AS LogonDescription,
get(item=EventData, field="SubjectUserName") AS InitiatingAccount,
-- LogonProcessName reveals the mechanism
get(item=EventData, field="LogonProcessName") AS AuthMechanism
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.EventID.Value = 4648 -- Explicit credential use (common in lateral movement)
OR System.EventID.Value = 4624 -- Successful logon
AND System.TimeCreated.SystemTime >= window_start
AND get(item=EventData, field="LogonType") IN ("3", "9", "10") -- Remote logon types
ORDER BY EventTime ASC
-- VQL: Look for PsExec, WMI exec, and other lateral movement tool signatures
-- These leave characteristic artefacts even when the tools themselves are gone
-- PsExec leaves a service named PSEXESVC
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="ServiceName") AS ServiceName,
get(item=EventData, field="ImagePath") AS ServiceBinary,
get(item=EventData, field="ServiceAccount") AS RunAsAccount,
"SERVICE_INSTALL" AS Indicator
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/System.evtx"
)
WHERE System.EventID.Value = 7045
AND System.TimeCreated.SystemTime >= window_start
UNION
-- WMI remote execution leaves Event 4688 with WmiPrvSE.exe as parent
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="NewProcessName") AS ServiceName,
get(item=EventData, field="CommandLine") AS ServiceBinary,
get(item=EventData, field="SubjectUserName") AS RunAsAccount,
"WMI_EXEC" AS Indicator
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.EventID.Value = 4688
AND get(item=EventData, field="ParentProcessName") =~ "WmiPrvSE"
AND System.TimeCreated.SystemTime >= window_start
ORDER BY EventTime ASC
Putting it all together: the complete investigation playbook
Running all of these queries individually is manageable during a focused investigation, but having them organised into a structured playbook makes the process repeatable and ensures nothing gets missed under pressure. The following VQL artefact wraps the entire multi-layer investigation into a single deployable workflow.
-- Complete investigation artefact definition
-- Save as Custom.Investigate.ProcessInjection.yaml
name: Custom.Investigate.ProcessInjection
description: |
Complete multi-layer investigation for a suspected process injection finding.
Correlates Velociraptor VAD findings with:
- Process ancestry and creation context (Sysmon Event 1)
- Security event timeline (logons, privilege use, process creation)
- Sysmon event timeline (network, files, registry, pipes)
- Filesystem artefacts (Prefetch, MFT)
- Current and historical network connections
- Lateral movement indicators
Run this against a specific host after the initial hunting query
identifies a suspicious PID.
author: justruss
type: CLIENT
parameters:
- name: SuspiciousPid
description: The PID identified as suspicious by the injection hunt
type: int
default: "0"
- name: InvestigationWindowMinutes
description: Minutes before and after process creation to include in timeline
type: int
default: "60"
sources:
- name: ProcessAncestry
description: Full process creation chain for the suspicious process
query: |
SELECT
ProcessId AS PID,
ParentProcessId AS ParentPID,
Name AS ProcessName,
CommandLine,
Username AS RunningAs,
CreateTime,
Exe AS BinaryPath,
hash(path=Exe, hashselect="SHA256") AS BinarySHA256,
hash(path=Exe, hashselect="SHA1") AS BinarySHA1
FROM pslist()
WHERE ProcessId = SuspiciousPid
OR ProcessId IN (
SELECT ParentProcessId FROM pslist()
WHERE ProcessId = SuspiciousPid
)
- name: VADMemoryRegions
description: All executable VAD regions for the suspicious process
query: |
SELECT
Pid,
Name AS ProcessName,
VAD.Start AS RegionStart,
format(format="0x%016X", args=VAD.Start) AS RegionStartHex,
VAD.End AS RegionEnd,
VAD.Protection AS Protection,
VAD.Type AS VADType,
VAD.FileObject AS BackingFile,
format(format="%d KB", args=[(VAD.End - VAD.Start) / 1024]) AS RegionSizeKB,
entropy(string=read_file(
accessor="process",
filename=format(format="/%d/%d", args=[Pid, VAD.Start]),
length=4096
)) AS EntropyScore,
if(condition=read_file(
accessor="process",
filename=format(format="/%d/%d", args=[Pid, VAD.Start]),
length=2
) = "MZ",
then="YES", else="NO") AS HasPEHeader,
if(condition=VAD.Protection = "EXECUTE_READ_WRITE",
then="CRITICAL", else="HIGH") AS RiskLevel
FROM foreach(
row={SELECT Pid, Name FROM pslist() WHERE Pid = SuspiciousPid},
query={
SELECT Pid, Name, VAD FROM vad(pid=Pid)
WHERE VAD.Protection =~ "EXECUTE"
AND VAD.Type = "PRIVATE"
AND (VAD.FileObject = NULL OR VAD.FileObject = "")
AND (VAD.End - VAD.Start) >= 4096
}
)
ORDER BY EntropyScore DESC
- name: ActiveThreads
description: Threads currently executing in suspicious memory regions
query: |
LET anon_vads = SELECT Pid, VAD.Start AS VStart, VAD.End AS VEnd
FROM foreach(
row={SELECT Pid FROM pslist() WHERE Pid = SuspiciousPid},
query={SELECT Pid, VAD FROM vad(pid=Pid)
WHERE VAD.Protection =~ "EXECUTE"
AND VAD.Type = "PRIVATE"
AND (VAD.FileObject = NULL OR VAD.FileObject = "")
AND (VAD.End - VAD.Start) >= 4096}
)
SELECT
t.Pid, t.Name AS ProcessName, t.ThreadId,
format(format="0x%016X", args=t.StartAddress) AS StartAddress,
format(format="0x%016X", args=v.VStart) AS VADStart,
"THREAD EXECUTING IN INJECTED REGION" AS Finding,
"CRITICAL" AS Severity
FROM foreach(
row={SELECT Pid, Name FROM pslist() WHERE Pid = SuspiciousPid},
query={SELECT Pid, Name, ThreadId, StartAddress FROM threads(pid=Pid)
WHERE StartAddress > 0x1000}
) AS t
JOIN anon_vads AS v ON (
t.Pid = v.Pid
AND t.StartAddress >= v.VStart
AND t.StartAddress < v.VEnd
)
- name: NetworkConnections
description: Current and recent network connections from suspicious process
query: |
SELECT
Pid,
Name AS ProcessName,
LocalAddress,
LocalPort,
RemoteAddress,
RemotePort,
Status,
now() AS CheckTime
FROM netstat()
WHERE Pid = SuspiciousPid
- name: RecentFileSystem
description: Files created or modified around the injection window
query: |
LET proc_info = SELECT CreateTime FROM pslist() WHERE Pid = SuspiciousPid
LET proc_time = proc_info[0].CreateTime
LET win_start = proc_time - InvestigationWindowMinutes * 60
LET win_end = proc_time + InvestigationWindowMinutes * 60
SELECT
FullPath AS FilePath,
Name AS FileName,
Mtime AS LastModified,
Ctime AS Created,
Size AS FileSizeBytes,
if(condition=Name =~ "(?i)\\.(exe|dll|ps1|vbs|js|bat|cmd|hta)$",
then="EXECUTABLE", else="DATA") AS FileType
FROM glob(globs=[
"C:/Users/*/AppData/Local/Temp/**",
"C:/Users/*/AppData/Roaming/**10",
"C:/Windows/Temp/**",
"C:/ProgramData/**5"
])
WHERE Mtime >= win_start
AND Mtime <= win_end
AND NOT IsDir
ORDER BY Mtime ASC
- name: PrefetchEvidence
description: Prefetch execution records for related processes
query: |
SELECT
Name AS PrefetchFile,
Mtime AS LastExecutionTime,
Ctime AS PrefetchCreated,
Size
FROM glob(globs="C:/Windows/Prefetch/*.pf")
ORDER BY Mtime DESC
LIMIT 50
Reading the complete investigation picture
When all six enrichment layers are combined, what you end up with is not just a list of suspicious memory regions it is a complete narrative of an intrusion. Walk through a realistic example of how to read the combined output.
The VAD query returns a 512KB RWX anonymous region in svchost.exe (PID 4892) with entropy of 7.3 and an MZ header. That is your starting point: injected PE file, currently in memory, high probability of being active malware.
The process ancestry query shows svchost.exe was created at 22:14:35 by services.exe, which is normal. But the command line is unusual it was launched with -k netsvcs -p -s Schedule rather than the typical short form. Cross-referencing with Sysmon Event ID 1 for the same PID shows that 22 seconds earlier at 22:14:13, a PowerShell process (PID 3842) with a base64-encoded command ran and terminated. PID 3842’s parent was winword.exe. That is the delivery chain: Office macro to PowerShell to shellcode injection into svchost.
The Sysmon timeline around 22:14:00 shows Event ID 22 (DNS query) at 22:14:08 for a domain that did not exist in the previous 30 days of DNS cache, followed by Event ID 3 (network connection) at 22:14:12 to a residential-range IP on port 443. Then Event ID 8 (CreateRemoteThread) at 22:14:34, source PID 3842, target PID 4892 this is the exact moment of injection. The PowerShell process injected into svchost.
The MFT query shows a file named update.ps1 created in C:\Users\jsmith\AppData\Local\Temp\ at 22:14:09 with a size of 4,847 bytes, and then a modification time identical to its creation time. It was written once and never modified a staging file. Prefetch confirms it ran at 22:14:11 with a run count of 1. The file is gone from disk but both the MFT entry and Prefetch prove its existence and execution.
The network connection query shows svchost.exe (PID 4892) has an established outbound connection to 203.0.113.45:443 that has been open for 47 minutes. Zeek’s ssl.log shows the JA3 hash for that connection is a0e9f5d64349fb13191bc781f81f42e1 the documented Cobalt Strike default JA3 hash.
The lateral movement query shows that at 22:31:06 17 minutes after injection Event ID 4648 fired showing jsmith‘s credentials used to authenticate to FINSERVER01 via a network logon. The scope just expanded from one workstation to a second host.
That is a complete incident in six data sources, stitched together through correlated timestamps and process identifiers. The initial Velociraptor VAD query found a thread. The enrichment queries built an entire story around it.
Enrichment layer 1: correlating VAD findings with Sysmon process creation history
Finding an anonymous executable region in a process is a strong indicator, but it answers only part of the question. You know something is in the memory of process X. You do not yet know how it got there, when it appeared, or what chain of events preceded it. Windows event logs and Sysmon telemetry fill that gap, giving you a timeline of everything that happened to that process before and after the injection.
The first enrichment step is to take the PID and process name from your Velociraptor findings and pull the full process creation chain from Sysmon Event ID 1. This tells you what created the process you are investigating, what command line was used, and how far back the process tree goes. An injected svchost.exe that was spawned by services.exe with normal arguments is a very different finding from an injected svchost.exe that was spawned by a PowerShell process with an encoded command argument.
-- VQL: Enrich suspicious process with its creation history from Sysmon EventLog
-- Replace TargetPid with the PID from your injection hunt results
LET target_pid = 4892 -- replace with suspicious PID from hunt results
-- Step 1: Get the full ancestry chain for the suspicious process
SELECT
ProcessId AS PID,
ParentProcessId AS ParentPID,
Name AS ProcessName,
CommandLine,
User,
CreateTime,
Exe AS ExecutablePath,
-- Hash the executable to check reputation
hash(path=Exe, hashselect="SHA256") AS ExecutableHash
FROM pslist()
WHERE ProcessId = target_pid
OR ProcessId IN (
-- Also pull the parent and grandparent
SELECT ParentProcessId FROM pslist() WHERE ProcessId = target_pid
)
-- VQL: Pull Sysmon Event ID 1 records for process creation context
-- This gives you the full command line and parent details at process start time
-- even if the process has been running for hours
SELECT
System.TimeCreated.SystemTime AS EventTime,
EventData.ProcessId AS PID,
EventData.ParentProcessId AS ParentPID,
EventData.Image AS ProcessImage,
EventData.CommandLine AS CommandLine,
EventData.ParentImage AS ParentImage,
EventData.ParentCommandLine AS ParentCommandLine,
EventData.User AS RunningAsUser,
EventData.IntegrityLevel AS IntegrityLevel,
EventData.Hashes AS Hashes,
-- Current directory at time of launch is often revealing
EventData.CurrentDirectory AS WorkingDirectory
FROM watch_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 1
AND (
EventData.ProcessId = format(format="%d", args=target_pid)
OR EventData.ParentProcessId = format(format="%d", args=target_pid)
)
ORDER BY EventTime ASC
Pay careful attention to the IntegrityLevel field in the Sysmon Event ID 1 record. A process running at High or System integrity that was spawned from a process at Medium integrity indicates a privilege escalation occurred somewhere in the chain. A process running as SYSTEM that was spawned from a user-context process through an unusual parent (WmiPrvSE.exe, msdtc.exe, or similar) indicates lateral movement or exploitation of a privileged service.
The Hashes field in Sysmon Event ID 1 contains the SHA256 of the executable at the time it launched. Take that hash and check it against threat intelligence immediately. If the binary has been renamed to disguise itself, the hash will still identify the real malware family even if the filename looks legitimate.
Enrichment layer 2: memory image acquisition and Volatility cross-analysis
Velociraptor can acquire a full memory image from a live remote system and make it available for offline analysis with Volatility. This is the bridge between the live query capabilities of Velociraptor and the deep structural analysis capabilities of Volatility. Where Velociraptor gives you fleet-wide fast triage, Volatility gives you the forensic depth to answer definitively what the injected code is, what it has been doing, and what it has communicated with.
-- VQL: Acquire full memory image from the target machine
-- Run this as a client artefact against the specific host that showed injection
SELECT * FROM Artifact.Windows.Memory.Acquisition(
-- Output path on the target machine (temporary staging)
destination="C:/Windows/Temp/mem_acquisition.raw",
-- Also acquire the pagefile for complete coverage
also_upload_pagefile=TRUE
)
Once the acquisition completes, Velociraptor uploads the image to the server and you can download it for Volatility analysis. The acquisition typically takes 2-10 minutes depending on RAM size. Do not wait for it to complete before starting other enrichment steps, the Velociraptor VAD queries already captured the volatile data you need for triage, so the memory image is for deep analysis that can run in parallel.
# Volatility 3 analysis workflow against the acquired image
# Replace memory.raw with your downloaded image filename
# Step 1: Confirm the image profile and get baseline info
vol -f memory.raw windows.info
# Step 2: Cross-reference the suspicious PID from Velociraptor
# Get the full process details including PPID chain
vol -f memory.raw windows.pstree | grep -A2 -B2 "4892"
# Step 3: Run malfind against the specific suspicious process
# malfind finds executable regions with no disk backing - same concept as our VQL
vol -f memory.raw windows.malfind --pid 4892
# Example malfind output for a Cobalt Strike injected process:
# Process: svchost.exe Pid: 4892 Address: 0x1d0000
# Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
# Hexdump:
# 4d 5a 90 00 03 00 00 00 MZ......
# This confirms the Velociraptor VAD finding with an independent second source
# Step 4: Dump the suspicious region for further analysis
vol -f memory.raw windows.dumpfiles --virtaddr 0x1d0000 --pid 4892 -o /tmp/dumped/
# Step 5: Analyse the dumped region
file /tmp/dumped/file.0x4892.0x1d0000.img
strings -a -n 8 /tmp/dumped/file.0x4892.0x1d0000.img | grep -iE "(http|https|192\.|10\.|beacon|sleep|checkin)"
sha256sum /tmp/dumped/file.0x4892.0x1d0000.img
# Step 6: Extract network connections from memory for C2 identification
vol -f memory.raw windows.netscan | grep "4892"
# Sample output:
# TCPv4 192.168.1.105 54321 198.51.100.45 443 ESTABLISHED 4892 svchost.exe
# This is the C2 connection - 198.51.100.45:443 is the attacker server
# Step 7: Check for credential material in the injected process
# If the injection target was lsass.exe or if the injected code accessed lsass
vol -f memory.raw windows.lsadump | head -50
# Step 8: Extract the full module list to find reflectively loaded components
vol -f memory.raw windows.dlllist --pid 4892
# Cross-reference against what Velociraptor reported in the modules() query
# Any discrepancy = something was loaded that the OS does not officially track
Enrichment layer 3: correlating with Windows event logs for timeline reconstruction
The memory analysis and Velociraptor VAD findings tell you what is in memory now. The Windows event logs tell you the story of how it got there. Building a complete timeline from event logs around the injection event turns an isolated finding into a full attack narrative that supports incident response decisions.
-- VQL: Pull all security-relevant events for the suspicious process
-- and the 30 minutes before and after it was created
-- This builds a timeline around the injection event
LET process_create_time = timestamp(string="2024-04-02T22:14:00Z") -- from Sysmon Event 1
LET window_start = process_create_time - 1800 -- 30 minutes before
LET window_end = process_create_time + 1800 -- 30 minutes after
SELECT
System.TimeCreated.SystemTime AS EventTime,
System.EventID.Value AS EventID,
-- Map event IDs to human-readable names
switch(
condition=System.EventID.Value = 4624, then="Logon Success",
condition=System.EventID.Value = 4625, then="Logon Failure",
condition=System.EventID.Value = 4648, then="Explicit Credentials Used",
condition=System.EventID.Value = 4688, then="Process Created",
condition=System.EventID.Value = 4689, then="Process Terminated",
condition=System.EventID.Value = 4698, then="Scheduled Task Created",
condition=System.EventID.Value = 7045, then="Service Installed",
condition=System.EventID.Value = 4720, then="User Account Created",
condition=System.EventID.Value = 4732, then="Member Added to Local Group",
else=format(format="Event %d", args=System.EventID.Value)
) AS EventName,
-- Pull the most relevant fields
get(item=EventData, field="SubjectUserName") AS Actor,
get(item=EventData, field="TargetUserName") AS Target,
get(item=EventData, field="ProcessName") AS ProcessName,
get(item=EventData, field="CommandLine") AS CommandLine,
get(item=EventData, field="IpAddress") AS SourceIP,
get(item=EventData, field="WorkstationName") AS SourceHost
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.TimeCreated.SystemTime >= window_start
AND System.TimeCreated.SystemTime <= window_end
AND System.EventID.Value IN (4624, 4625, 4648, 4688, 4689, 4698, 7045, 4720, 4732)
ORDER BY EventTime ASC
-- VQL: Sysmon timeline for the injection window
-- Covers network connections, file operations, registry changes, and pipe creation
-- All correlated to the same time window around the injection
SELECT
System.TimeCreated.SystemTime AS EventTime,
System.EventID.Value AS SysmonEventID,
switch(
condition=System.EventID.Value = 1, then="Process Created",
condition=System.EventID.Value = 3, then="Network Connection",
condition=System.EventID.Value = 5, then="Process Terminated",
condition=System.EventID.Value = 6, then="Driver Loaded",
condition=System.EventID.Value = 7, then="Image Loaded",
condition=System.EventID.Value = 8, then="CreateRemoteThread",
condition=System.EventID.Value = 10, then="ProcessAccess (LSASS/injection)",
condition=System.EventID.Value = 11, then="File Created",
condition=System.EventID.Value = 12, then="Registry Key Created/Deleted",
condition=System.EventID.Value = 13, then="Registry Value Set",
condition=System.EventID.Value = 17, then="Pipe Created",
condition=System.EventID.Value = 18, then="Pipe Connected",
condition=System.EventID.Value = 22, then="DNS Query",
condition=System.EventID.Value = 25, then="Process Tampering",
else=format(format="Sysmon %d", args=System.EventID.Value)
) AS EventType,
get(item=EventData, field="Image") AS ProcessImage,
get(item=EventData, field="TargetImage") AS TargetProcess,
get(item=EventData, field="DestinationIp") AS DestIP,
get(item=EventData, field="DestinationPort") AS DestPort,
get(item=EventData, field="QueryName") AS DNSQuery,
get(item=EventData, field="TargetFilename") AS FilePath,
get(item=EventData, field="TargetObject") AS RegistryKey,
get(item=EventData, field="PipeName") AS PipeName,
get(item=EventData, field="GrantedAccess") AS AccessMask
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.TimeCreated.SystemTime >= window_start
AND System.TimeCreated.SystemTime <= window_end
ORDER BY EventTime ASC
When you view this combined timeline, you are looking for the moment the injection happened and everything that preceded it. Work backwards from the injection event. What network connections were made immediately before? What processes were created? Was there a suspicious DNS query 30 seconds before the injected process started? Was there a logon event from an unusual source IP around the same time?
Common patterns you will see in real injection chains: a macro-enabled Office document spawning PowerShell (Event ID 1 with ParentImage=winword.exe and Image=powershell.exe), followed immediately by a DNS query for a domain that resolves to a known hosting provider (Event ID 22), followed by a network connection outbound on port 443 (Event ID 3), followed by a CreateRemoteThread event (Event ID 8) into a separate process. That four-event sequence, compressed into 10-30 seconds, is the macro-to-shellcode-to-injected-process chain in plain text.
Enrichment layer 4: filesystem artefacts, Prefetch, Amcache, and MFT
After establishing what is in memory and correlating with event logs, the filesystem artefacts provide a third independent source of evidence that can confirm or add detail to the picture. Prefetch files tell you what ran and when, Amcache provides execution hashes for tools that have since been deleted, and the MFT timestamps tell you when files appeared and changed on disk.
-- VQL: Pull Prefetch execution history for processes related to the injection
-- Prefetch files survive even after the executable is deleted
SELECT
Name AS PrefetchFile,
-- Extract process name from prefetch filename (format: PROCESSNAME-HASH.pf)
regex_transform(
source=Name,
map={"^(.+)-[A-F0-9]{8}\\.pf$": "$1"}
) AS ExecutableName,
Mtime AS LastExecutionTime,
Ctime AS FirstSeen,
-- Read the prefetch binary for run count and timestamps
-- (basic metadata only without full parsing)
Size AS FileSize
FROM glob(
globs="C:/Windows/Prefetch/*.pf"
)
-- Filter to processes related to the injection investigation
WHERE ExecutableName IN (
"POWERSHELL",
"CMD",
"WSCRIPT",
"CSCRIPT",
"MSHTA",
"REGSVR32",
"RUNDLL32",
-- Add the specific process name from your injection findings
"SVCHOST" -- example
)
ORDER BY LastExecutionTime DESC
-- VQL: Amcache analysis for executed files with hash attribution
-- Amcache persists SHA1 hashes of files that have executed
-- even if those files have since been deleted
SELECT
BinProductVersion AS FileVersion,
BinFileDescription AS FileDescription,
FileId AS SHA1Hash,
LongPathHash AS FilePath,
LinkDate AS CompileTimestamp,
-- The write time on the Amcache key approximates first execution time
Modified AS ApproxFirstExecution
FROM read_reg_key(
globs="HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/AppCompatCache"
)
-- Alternatively, parse the Amcache.hve directly for richer data
-- VQL to invoke AmcacheParser via shell
SELECT * FROM execve(argv=[
"C:/tools/AmcacheParser.exe",
"-f", "C:/Windows/AppCompat/Programs/Amcache.hve",
"--csv", "C:/Windows/Temp/amcache_output",
"-q"
])
WHERE ReturnCode = 0
-- VQL: MFT timeline analysis around the injection window
-- The MFT records precise timestamps for every file creation, modification,
-- and access -- revealing dropped tools, staged payloads, and cleanup attempts
SELECT
FullPath AS FilePath,
Name AS FileName,
-- NTFS records four timestamps per file (MACB)
SI_Mtime AS Modified,
SI_Atime AS Accessed,
SI_Ctime AS MetadataChanged,
SI_Btime AS Created,
FileSize,
IsDir,
-- Flag executables and scripts specifically
if(condition=Name =~ "(?i)\\.(exe|dll|ps1|vbs|js|bat|cmd|hta|scr)$",
then="EXECUTABLE", else="DATA") AS FileType
FROM parse_mft(
-- Direct MFT access requires admin privileges
filename="C:/$MFT",
accessor="ntfs"
)
WHERE
-- Focus on the 2-hour window around the injection
SI_Btime >= window_start
AND SI_Btime <= window_end
-- Focus on likely staging and tool-drop locations
AND (
FullPath =~ "(?i)(\\Temp\\|\\AppData\\|\\ProgramData\\|\\Public\\)"
OR FullPath =~ "(?i)(\\Downloads\\|\\Desktop\\)"
)
AND NOT IsDir
ORDER BY SI_Btime ASC
The MFT timeline is particularly valuable for finding files that were created, used briefly, and deleted, a pattern that is invisible in most log sources but leaves a permanent MFT record. Attackers who drop a tool, run it, and delete it often miss the fact that the MFT entry persists long after the file is gone. Cross-reference MFT creation timestamps with Prefetch execution timestamps for the same filename: if a file was created at 22:14:31 and Prefetch shows it ran at 22:14:35 with a single run count, it was a one-shot tool that was deleted after use. That four-second gap between file creation and first execution is the attacker staging and immediately running a tool.
Enrichment layer 5: network telemetry correlation
The injected process almost always makes network connections, that is usually the whole point. Correlating the memory and event log findings with network telemetry from Zeek or your firewall gives you the C2 infrastructure details that complete the attack picture. You now know not just that a machine is compromised, but exactly where it is communicating with and what that communication looks like.
-- VQL: Pull all network connections made by the suspicious process
-- Cross-reference with Sysmon network events and live connections
-- Current network connections (live state)
SELECT
Pid,
Name AS ProcessName,
LocalAddress,
LocalPort,
RemoteAddress,
RemotePort,
Status AS ConnectionState,
-- Flag obviously suspicious destinations
if(condition=RemotePort IN (4444, 8080, 8443, 1337, 31337),
then="COMMON_C2_PORT", else="CHECK") AS PortFlag
FROM netstat()
WHERE Pid = target_pid
UNION
-- Historical connections from Sysmon Event ID 3
SELECT
get(item=EventData, field="ProcessId") AS Pid,
get(item=EventData, field="Image") AS ProcessName,
get(item=EventData, field="SourceIp") AS LocalAddress,
get(item=EventData, field="SourcePort") AS LocalPort,
get(item=EventData, field="DestinationIp") AS RemoteAddress,
get(item=EventData, field="DestinationPort") AS RemotePort,
"HISTORICAL" AS ConnectionState,
get(item=EventData, field="UtcTime") AS EventTime
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 3
AND get(item=EventData, field="ProcessId") = format(format="%d", args=target_pid)
ORDER BY EventTime DESC
-- VQL: DNS query history for the suspicious process
-- Sysmon Event ID 22 captures DNS queries with the requesting process
-- This gives you the domain names the injected code tried to resolve
SELECT
System.TimeCreated.SystemTime AS QueryTime,
get(item=EventData, field="QueryName") AS Domain,
get(item=EventData, field="QueryResults") AS ResolvedIPs,
get(item=EventData, field="Image") AS RequestingProcess,
-- Flag high-risk domain patterns
if(condition=get(item=EventData, field="QueryName") =~
"(?i)(\.tk$|\.xyz$|\.top$|bit\.ly|tinyurl|ngrok\.io|pastebin)",
then="SUSPICIOUS_TLD", else="CHECK") AS DomainFlag
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Microsoft-Windows-Sysmon%4Operational.evtx"
)
WHERE System.EventID.Value = 22
AND get(item=EventData, field="Image") =~ "svchost" -- match process from findings
AND System.TimeCreated.SystemTime >= window_start
ORDER BY QueryTime ASC
# Zeek correlation: match the C2 IP from memory analysis against network logs
# Run this on your Zeek sensor after identifying the C2 IP from Volatility netscan
C2_IP="198.51.100.45"
WINDOW_START="2024-04-02T22:00:00"
WINDOW_END="2024-04-02T23:00:00"
# All connections to the C2 IP in the incident window
cat /opt/zeek/logs/current/conn.log | \
python3 -c "
import json, sys
from datetime import datetime
c2_ip = '$C2_IP'
for line in sys.stdin:
try:
rec = json.loads(line)
if rec.get('id.resp_h') == c2_ip or rec.get('id.orig_h') == c2_ip:
print(json.dumps({
'time': rec.get('ts'),
'src': rec.get('id.orig_h'),
'src_port': rec.get('id.orig_p'),
'dst': rec.get('id.resp_h'),
'dst_port': rec.get('id.resp_p'),
'duration': rec.get('duration'),
'bytes_out': rec.get('orig_bytes'),
'bytes_in': rec.get('resp_bytes'),
'protocol': rec.get('proto'),
'state': rec.get('conn_state')
}, indent=2))
except:
pass
"
# TLS session details if C2 uses HTTPS
cat /opt/zeek/logs/current/ssl.log | \
python3 -c "
import json, sys
for line in sys.stdin:
try:
rec = json.loads(line)
if rec.get('id.resp_h') == '$C2_IP':
print(f\"JA3: {rec.get('ja3','none')} | SNI: {rec.get('server_name','none')} | Cert: {rec.get('subject','none')}\")
except:
pass
"
The combination of the destination IP from netscan, the JA3 fingerprint from Zeek’s ssl.log, and the DNS query history from Sysmon Event ID 22 gives you a complete C2 profile. Submit the JA3 hash to threat intelligence platforms, if it matches a known Cobalt Strike or Meterpreter default, that narrows the toolset significantly. Take the destination IP and check it against passive DNS to find other domains that have resolved to the same infrastructure. Those other domains may be used in other campaigns targeting your sector.
Enrichment layer 6: lateral movement indicators
A compromised machine is rarely the end of the story. Once an attacker has injected code and established C2, the next phase is almost always lateral movement. Looking for signs that the injected process was used as a pivot point, or that the credentials it accessed were used elsewhere, rounds out the investigation and determines scope.
-- VQL: Detect lateral movement from the compromised host
-- Look for authentication attempts to other systems originating from this host
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="TargetUserName") AS AccountUsed,
get(item=EventData, field="TargetServerName") AS TargetSystem,
get(item=EventData, field="LogonType") AS LogonType,
switch(
condition=get(item=EventData, field="LogonType") = "3",
then="Network (SMB/WMI/DCOM)",
condition=get(item=EventData, field="LogonType") = "10",
then="Remote Interactive (RDP)",
condition=get(item=EventData, field="LogonType") = "9",
then="NewCredentials (runas /netonly)",
else=format(format="Type %v", args=get(item=EventData, field="LogonType"))
) AS LogonDescription,
get(item=EventData, field="SubjectUserName") AS InitiatingAccount,
-- LogonProcessName reveals the mechanism
get(item=EventData, field="LogonProcessName") AS AuthMechanism
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.EventID.Value = 4648 -- Explicit credential use (common in lateral movement)
OR System.EventID.Value = 4624 -- Successful logon
AND System.TimeCreated.SystemTime >= window_start
AND get(item=EventData, field="LogonType") IN ("3", "9", "10") -- Remote logon types
ORDER BY EventTime ASC
-- VQL: Look for PsExec, WMI exec, and other lateral movement tool signatures
-- These leave characteristic artefacts even when the tools themselves are gone
-- PsExec leaves a service named PSEXESVC
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="ServiceName") AS ServiceName,
get(item=EventData, field="ImagePath") AS ServiceBinary,
get(item=EventData, field="ServiceAccount") AS RunAsAccount,
"SERVICE_INSTALL" AS Indicator
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/System.evtx"
)
WHERE System.EventID.Value = 7045
AND System.TimeCreated.SystemTime >= window_start
UNION
-- WMI remote execution leaves Event 4688 with WmiPrvSE.exe as parent
SELECT
System.TimeCreated.SystemTime AS EventTime,
get(item=EventData, field="NewProcessName") AS ServiceName,
get(item=EventData, field="CommandLine") AS ServiceBinary,
get(item=EventData, field="SubjectUserName") AS RunAsAccount,
"WMI_EXEC" AS Indicator
FROM parse_evtx(
filename="C:/Windows/System32/winevt/Logs/Security.evtx"
)
WHERE System.EventID.Value = 4688
AND get(item=EventData, field="ParentProcessName") =~ "WmiPrvSE"
AND System.TimeCreated.SystemTime >= window_start
ORDER BY EventTime ASC
Putting it all together: the complete investigation playbook
Running all of these queries individually is manageable during a focused investigation, but having them organised into a structured playbook makes the process repeatable and ensures nothing gets missed under pressure. The following VQL artefact wraps the entire multi-layer investigation into a single deployable workflow.
-- Complete investigation artefact definition
-- Save as Custom.Investigate.ProcessInjection.yaml
name: Custom.Investigate.ProcessInjection
description: |
Complete multi-layer investigation for a suspected process injection finding.
Correlates Velociraptor VAD findings with:
- Process ancestry and creation context (Sysmon Event 1)
- Security event timeline (logons, privilege use, process creation)
- Sysmon event timeline (network, files, registry, pipes)
- Filesystem artefacts (Prefetch, MFT)
- Current and historical network connections
- Lateral movement indicators
Run this against a specific host after the initial hunting query
identifies a suspicious PID.
author: justruss
type: CLIENT
parameters:
- name: SuspiciousPid
description: The PID identified as suspicious by the injection hunt
type: int
default: "0"
- name: InvestigationWindowMinutes
description: Minutes before and after process creation to include in timeline
type: int
default: "60"
sources:
- name: ProcessAncestry
description: Full process creation chain for the suspicious process
query: |
SELECT
ProcessId AS PID,
ParentProcessId AS ParentPID,
Name AS ProcessName,
CommandLine,
Username AS RunningAs,
CreateTime,
Exe AS BinaryPath,
hash(path=Exe, hashselect="SHA256") AS BinarySHA256,
hash(path=Exe, hashselect="SHA1") AS BinarySHA1
FROM pslist()
WHERE ProcessId = SuspiciousPid
OR ProcessId IN (
SELECT ParentProcessId FROM pslist()
WHERE ProcessId = SuspiciousPid
)
- name: VADMemoryRegions
description: All executable VAD regions for the suspicious process
query: |
SELECT
Pid,
Name AS ProcessName,
VAD.Start AS RegionStart,
format(format="0x%016X", args=VAD.Start) AS RegionStartHex,
VAD.End AS RegionEnd,
VAD.Protection AS Protection,
VAD.Type AS VADType,
VAD.FileObject AS BackingFile,
format(format="%d KB", args=[(VAD.End - VAD.Start) / 1024]) AS RegionSizeKB,
entropy(string=read_file(
accessor="process",
filename=format(format="/%d/%d", args=[Pid, VAD.Start]),
length=4096
)) AS EntropyScore,
if(condition=read_file(
accessor="process",
filename=format(format="/%d/%d", args=[Pid, VAD.Start]),
length=2
) = "MZ",
then="YES", else="NO") AS HasPEHeader,
if(condition=VAD.Protection = "EXECUTE_READ_WRITE",
then="CRITICAL", else="HIGH") AS RiskLevel
FROM foreach(
row={SELECT Pid, Name FROM pslist() WHERE Pid = SuspiciousPid},
query={
SELECT Pid, Name, VAD FROM vad(pid=Pid)
WHERE VAD.Protection =~ "EXECUTE"
AND VAD.Type = "PRIVATE"
AND (VAD.FileObject = NULL OR VAD.FileObject = "")
AND (VAD.End - VAD.Start) >= 4096
}
)
ORDER BY EntropyScore DESC
- name: ActiveThreads
description: Threads currently executing in suspicious memory regions
query: |
LET anon_vads = SELECT Pid, VAD.Start AS VStart, VAD.End AS VEnd
FROM foreach(
row={SELECT Pid FROM pslist() WHERE Pid = SuspiciousPid},
query={SELECT Pid, VAD FROM vad(pid=Pid)
WHERE VAD.Protection =~ "EXECUTE"
AND VAD.Type = "PRIVATE"
AND (VAD.FileObject = NULL OR VAD.FileObject = "")
AND (VAD.End - VAD.Start) >= 4096}
)
SELECT
t.Pid, t.Name AS ProcessName, t.ThreadId,
format(format="0x%016X", args=t.StartAddress) AS StartAddress,
format(format="0x%016X", args=v.VStart) AS VADStart,
"THREAD EXECUTING IN INJECTED REGION" AS Finding,
"CRITICAL" AS Severity
FROM foreach(
row={SELECT Pid, Name FROM pslist() WHERE Pid = SuspiciousPid},
query={SELECT Pid, Name, ThreadId, StartAddress FROM threads(pid=Pid)
WHERE StartAddress > 0x1000}
) AS t
JOIN anon_vads AS v ON (
t.Pid = v.Pid
AND t.StartAddress >= v.VStart
AND t.StartAddress < v.VEnd
)
- name: NetworkConnections
description: Current and recent network connections from suspicious process
query: |
SELECT
Pid,
Name AS ProcessName,
LocalAddress,
LocalPort,
RemoteAddress,
RemotePort,
Status,
now() AS CheckTime
FROM netstat()
WHERE Pid = SuspiciousPid
- name: RecentFileSystem
description: Files created or modified around the injection window
query: |
LET proc_info = SELECT CreateTime FROM pslist() WHERE Pid = SuspiciousPid
LET proc_time = proc_info[0].CreateTime
LET win_start = proc_time - InvestigationWindowMinutes * 60
LET win_end = proc_time + InvestigationWindowMinutes * 60
SELECT
FullPath AS FilePath,
Name AS FileName,
Mtime AS LastModified,
Ctime AS Created,
Size AS FileSizeBytes,
if(condition=Name =~ "(?i)\\.(exe|dll|ps1|vbs|js|bat|cmd|hta)$",
then="EXECUTABLE", else="DATA") AS FileType
FROM glob(globs=[
"C:/Users/*/AppData/Local/Temp/**",
"C:/Users/*/AppData/Roaming/**10",
"C:/Windows/Temp/**",
"C:/ProgramData/**5"
])
WHERE Mtime >= win_start
AND Mtime <= win_end
AND NOT IsDir
ORDER BY Mtime ASC
- name: PrefetchEvidence
description: Prefetch execution records for related processes
query: |
SELECT
Name AS PrefetchFile,
Mtime AS LastExecutionTime,
Ctime AS PrefetchCreated,
Size
FROM glob(globs="C:/Windows/Prefetch/*.pf")
ORDER BY Mtime DESC
LIMIT 50
Reading the complete investigation picture
When all six enrichment layers are combined, what you end up with is not just a list of suspicious memory regions, it is a complete narrative of an intrusion. Walk through a realistic example of how to read the combined output.
The VAD query returns a 512KB RWX anonymous region in svchost.exe (PID 4892) with entropy of 7.3 and an MZ header. That is your starting point: injected PE file, currently in memory, high probability of being active malware.
The process ancestry query shows svchost.exe was created at 22:14:35 by services.exe, which is normal. But the command line is unusual, it was launched with -k netsvcs -p -s Schedule rather than the typical short form. Cross-referencing with Sysmon Event ID 1 for the same PID shows that 22 seconds earlier at 22:14:13, a PowerShell process (PID 3842) with a base64-encoded command ran and terminated. PID 3842’s parent was winword.exe. That is the delivery chain: Office macro to PowerShell to shellcode injection into svchost.
The Sysmon timeline around 22:14:00 shows Event ID 22 (DNS query) at 22:14:08 for a domain that did not exist in the previous 30 days of DNS cache, followed by Event ID 3 (network connection) at 22:14:12 to a residential-range IP on port 443. Then Event ID 8 (CreateRemoteThread) at 22:14:34, source PID 3842, target PID 4892, this is the exact moment of injection. The PowerShell process injected into svchost.
The MFT query shows a file named update.ps1 created in C:\Users\jsmith\AppData\Local\Temp\ at 22:14:09 with a size of 4,847 bytes, and then a modification time identical to its creation time. It was written once and never modified, a staging file. Prefetch confirms it ran at 22:14:11 with a run count of 1. The file is gone from disk but both the MFT entry and Prefetch prove its existence and execution.
The network connection query shows svchost.exe (PID 4892) has an established outbound connection to 203.0.113.45:443 that has been open for 47 minutes. Zeek’s ssl.log shows the JA3 hash for that connection is a0e9f5d64349fb13191bc781f81f42e1, the documented Cobalt Strike default JA3 hash.
The lateral movement query shows that at 22:31:06, 17 minutes after injection, Event ID 4648 fired showing jsmith‘s credentials used to authenticate to FINSERVER01 via a network logon. The scope just expanded from one workstation to a second host.
That is a complete incident in six data sources, stitched together through correlated timestamps and process identifiers. The initial Velociraptor VAD query found a thread. The enrichment queries built an entire story around it.