{"id":298,"date":"2026-05-05T09:00:00","date_gmt":"2026-05-05T09:00:00","guid":{"rendered":"http:\/\/justruss.tech\/index.php\/2025\/05\/20\/using-velociraptor-to-hunt-injected-processes-finding-executables-not-backed-by-disk\/"},"modified":"2026-05-15T10:34:55","modified_gmt":"2026-05-15T10:34:55","slug":"using-velociraptor-to-hunt-injected-processes-finding-executables-not-backed-by-disk","status":"publish","type":"post","link":"https:\/\/justruss.tech\/index.php\/2026\/05\/05\/using-velociraptor-to-hunt-injected-processes-finding-executables-not-backed-by-disk\/","title":{"rendered":"Velociraptor Process Injection Hunting: Finding Executables Not Backed by Disk"},"content":{"rendered":"<p>Process injection detection is one of the most valuable capabilities a threat hunter can build into their regular workflow. When an attacker injects shellcode or a reflectively loaded DLL into a legitimate process, that process continues to look completely normal by almost every standard check, it has the right name, the right path, the right digital signature, and appears in the process list exactly as expected. What gives it away is what is happening inside its memory: executable regions that have no corresponding file on disk, mapped sections whose content does not match the file they claim to be backed by, and threads executing code from anonymous memory allocations.<\/p>\n<p>Velociraptor is particularly well-suited for this type of hunting because it can interrogate live process memory across an entire fleet simultaneously, run structured VQL queries against the results, and return findings within minutes rather than the hours it would take to manually collect and analyse artefacts from individual machines. This post walks through building a complete process injection hunting workflow in Velociraptor from first principles.<\/p>\n<h3>Understanding what we are hunting for<\/h3>\n<p>Before writing a single line of VQL, it helps to be precise about what injected code looks like in memory and why. Windows maps memory regions using a structure called the Virtual Address Descriptor (VAD) tree. Every allocation in a process&#8217;s virtual address space has a VAD entry that records the protection flags (readable, writable, executable), whether the region is private or shared, and if it is backed by a file, which file. When Windows loads a legitimate DLL or executable, the VAD entry for that code region points to the file on disk. The on-disk file is the backing store.<\/p>\n<p>Injected code breaks this relationship. Shellcode injected with VirtualAllocEx creates a VAD entry with executable permissions but no file backing, it is anonymous private memory containing executable code. A reflectively loaded DLL creates a VAD entry that looks like a file-backed mapping but the content in memory differs from any file on disk. Process hollowing creates a VAD entry backed by the legitimate executable&#8217;s file path, but the memory contents have been replaced with the attacker&#8217;s code.<\/p>\n<p>Each of these has a distinct memory signature, and Velociraptor can query all of them.<\/p>\n<h3>Setting up the investigation context<\/h3>\n<p>Before running any VQL, establish a baseline understanding of your environment. On a freshly built Windows 10 or 11 system, even with normal software installed, there will be some legitimate executable anonymous memory, the .NET JIT compiler generates executable code in anonymous allocations, some video drivers do the same, and certain legitimate protection mechanisms use RWX memory. The goal is not to eliminate all findings but to understand what is normal so you can identify what is not.<\/p>\n<p>In the Velociraptor GUI, navigate to the Hunt Manager and create a new hunt. For initial scoping, run against a small subset of machines, a few representative workstations and a server or two, before deploying fleet-wide. This gives you a calibration dataset to work from.<\/p>\n<h3>Artefact 1: VAD regions with executable permissions and no file backing<\/h3>\n<p>The most direct way to find injected shellcode is to look for VAD regions that are marked executable (or execute-read-write) but have no file backing them. In Velociraptor, the <code>proc_dump()<\/code> and <code>vad()<\/code> plugins give access to this information.<\/p>\n<pre>-- Query 1: Find executable anonymous VAD regions across all processes\n-- This is the primary injection indicator query\n\nSELECT\n Pid,\n Name AS ProcessName,\n Exe AS ProcessPath,\n -- VAD details\n VAD.Start AS RegionStart,\n VAD.End AS RegionEnd,\n VAD.Protection AS Protection,\n VAD.Type AS VADType,\n VAD.FileObject AS BackingFile,\n -- Size in KB for readability\n format(format=\"%d KB\", args=[(VAD.End - VAD.Start) \/ 1024]) AS RegionSize,\n -- Entropy of the region (high entropy = compressed or encrypted content)\n entropy(string=read_file(\n accessor=\"process\",\n filename=format(format=\"\/%d\/%d\", args=[Pid, VAD.Start]),\n length=4096\n )) AS SectionEntropy\n\nFROM foreach(\n row={\n -- Get all running processes\n SELECT Pid, Name, Exe FROM pslist()\n WHERE Pid &gt; 4 -- Exclude System process (PID 4)\n },\n query={\n -- Get VAD entries for each process\n SELECT Pid, Name, Exe, VAD\n FROM vad(pid=Pid)\n WHERE\n -- Must have execute permission\n VAD.Protection =~ \"EXECUTE\"\n -- Must NOT be backed by a file on disk\n AND VAD.Type = \"PRIVATE\"\n AND (VAD.FileObject = NULL OR VAD.FileObject = \"\")\n }\n)\n\n-- Filter out known legitimate patterns\nWHERE\n -- Exclude very small regions (less than 4KB, often legitimate JIT stubs)\n (VAD.End - VAD.Start) &gt; 4096\n -- Exclude known-legitimate anonymous executable memory patterns\n AND NOT ProcessName IN (\"MsMpEng.exe\", \"SgrmBroker.exe\")\n\nORDER BY SectionEntropy DESC, (VAD.End - VAD.Start) DESC<\/pre>\n<p>When you run this query, you will get a table of results. Before panicking about every row, understand what the columns mean. The <code>Protection<\/code> field will show values like <code>EXECUTE_READ_WRITE<\/code> (RWX memory, the most suspicious), <code>EXECUTE_READ<\/code> (readable and executable, less suspicious but still worth reviewing), or <code>EXECUTE_WRITECOPY<\/code>. RWX memory is the classic shellcode staging pattern because the attacker needs to write the shellcode in, then execute it. Legitimate code regions are almost always EXECUTE_READ only, they are written to disk during compilation, not at runtime.<\/p>\n<p>The <code>SectionEntropy<\/code> column is particularly useful for triage. Entropy measures the randomness of the data. Encrypted or compressed shellcode (which most real-world shellcode is) has entropy close to 8.0. Plain English text has entropy around 4.0. Compiled x86\/x64 code without encryption typically sits around 5.5 to 6.5. A small RWX region with entropy above 7.0 should be near the top of your investigation list.<\/p>\n<h3>Artefact 2: PE headers in anonymous memory (reflective loading)<\/h3>\n<p>Reflective DLL injection loads a full PE file into anonymous memory. You can detect this by reading the first bytes of each executable anonymous region and checking for the MZ header (0x4D 0x5A) that marks the start of a PE file. A PE file in anonymous executable memory with no disk backing is a very strong indicator of reflective loading.<\/p>\n<pre>-- Query 2: Find PE files loaded in anonymous executable memory\n-- MZ header (4D 5A) = Windows executable or DLL\n\nSELECT\n Pid,\n Name AS ProcessName,\n Exe AS ProcessPath,\n VAD.Start AS RegionStart,\n format(format=\"0x%08X\", args=VAD.Start) AS RegionStartHex,\n VAD.Protection AS Protection,\n format(format=\"%d KB\", args=[(VAD.End - VAD.Start) \/ 1024]) AS RegionSize,\n -- Read first 2 bytes to check for MZ header\n format(format=\"%02X %02X\",\n args=[\n ord(read_file(\n accessor=\"process\",\n filename=format(format=\"\/%d\/%d\", args=[Pid, VAD.Start]),\n length=1\n )),\n ord(read_file(\n accessor=\"process\",\n filename=format(format=\"\/%d\/%d\", args=[Pid, VAD.Start + 1]),\n length=1\n ))\n ]\n ) AS FirstTwoBytes,\n -- Also check for PE signature at the offset indicated by MZ header\n VAD.FileObject AS BackingFile\n\nFROM foreach(\n row={SELECT Pid, Name, Exe FROM pslist() WHERE Pid &gt; 4},\n query={\n SELECT Pid, Name, Exe, VAD\n FROM vad(pid=Pid)\n WHERE\n VAD.Protection =~ \"EXECUTE\"\n AND VAD.Type = \"PRIVATE\"\n AND (VAD.FileObject = NULL OR VAD.FileObject = \"\")\n AND (VAD.End - VAD.Start) &gt; 4096\n }\n)\nWHERE FirstTwoBytes = \"4D 5A\" -- MZ header\n\nORDER BY (VAD.End - VAD.Start) DESC<\/pre>\n<p>Results from this query where <code>FirstTwoBytes = \"4D 5A\"<\/code> are worth immediate investigation. Common legitimate sources of PE headers in anonymous memory include the .NET CLR (which loads assemblies this way in some configurations) and some anti-cheat systems. Most environments will have very few results here, and any process that is not expected to use reflection-based loading showing up is a priority investigation target.<\/p>\n<h3>Artefact 3: Module stomping, memory content differs from disk<\/h3>\n<p>Module stomping is harder to detect than anonymous injection because the VAD entry does point to a real file. The malicious content has overwritten what was in memory, but the VAD still says the region is backed by, say, <code>C:\\Windows\\System32\\version.dll<\/code>. The way to catch this is to read both the on-disk version of the file and the in-memory version, hash them, and compare. Any discrepancy in the executable sections indicates the memory has been modified after the legitimate module was loaded.<\/p>\n<pre>-- Query 3: Detect module stomping by comparing in-memory content to on-disk file\n-- This is computationally expensive - run against suspicious processes identified above\n-- or against high-value targets (LSASS, explorer.exe, svchost.exe instances)\n\nSELECT\n Pid,\n Name AS ProcessName,\n ModuleBase,\n ModuleName,\n ModulePath,\n -- Hash of the .text section as loaded in memory\n hash(path=format(format=\"\/%d\/%d\", args=[Pid, ModuleBase]),\n accessor=\"process\",\n hashselect=\"SHA256\") AS MemoryHash,\n -- Hash of the same file on disk\n hash(path=ModulePath, hashselect=\"SHA256\") AS DiskHash,\n -- Flag if they differ\n if(condition=MemoryHash != DiskHash,\n then=\"MISMATCH - POSSIBLE STOMPING\",\n else=\"OK\") AS Status\n\nFROM foreach(\n row={\n -- Focus on specific high-value processes first\n SELECT Pid, Name, Exe\n FROM pslist()\n WHERE Name IN (\"explorer.exe\", \"svchost.exe\", \"lsass.exe\",\n \"winlogon.exe\", \"services.exe\", \"spoolsv.exe\")\n },\n query={\n SELECT\n Pid, Name, Exe,\n ModuleBase,\n BaseName AS ModuleName,\n FullPath AS ModulePath\n FROM modules(pid=Pid)\n WHERE ModulePath =~ \"\\.dll$\"\n AND ModulePath =~ \"(?i)(System32|SysWOW64)\"\n }\n)\nWHERE Status = \"MISMATCH - POSSIBLE STOMPING\"\n\nORDER BY Pid, ModuleName<\/pre>\n<p>This query is deliberately scoped to specific high-value processes and system DLLs. Running it across every module in every process would be extremely slow and generate significant noise. The approach is to use the previous two queries to identify suspicious processes first, then use this query to investigate those specific processes in depth.<\/p>\n<p>When you get a mismatch result, the first thing to verify is that it is not a legitimate hotpatch. Windows uses in-memory patching for some security updates. Cross-reference the mismatch with recent Windows Update activity. If the hash mismatch appeared before any recent update, or affects a module that is not part of the Windows hotpatch mechanism, treat it as an active finding.<\/p>\n<h3>Artefact 4: Threads executing from suspicious memory regions<\/h3>\n<p>The VAD queries above find the memory regions. But it is also worth knowing whether there are currently active threads executing code from those suspicious regions. A thread with its instruction pointer (EIP\/RIP) inside an anonymous executable memory region is direct evidence of active injected code execution, not just staged shellcode waiting to run.<\/p>\n<pre>-- Query 4: Find threads executing from anonymous or unusual memory regions\n-- The Start Address of a thread should point into a known loaded module\n\nSELECT\n Pid,\n Name AS ProcessName,\n ThreadId AS TID,\n format(format=\"0x%016X\", args=StartAddress) AS ThreadStartAddress,\n -- Determine which module (if any) the start address falls within\n -- If no module contains this address, the thread is executing from anonymous memory\n regex_transform(\n source=format(format=\"%d\", args=StartAddress),\n map={\n \"0\": \"ANONYMOUS_MEMORY_EXECUTION\"\n }\n ) AS ExecutionSource\n\nFROM foreach(\n row={SELECT Pid, Name FROM pslist() WHERE Pid &gt; 4},\n query={\n SELECT Pid, Name, ThreadId, StartAddress\n FROM threads(pid=Pid)\n }\n)\n-- A start address of 0 or very low values indicates an anomaly\nWHERE StartAddress &gt; 0\n AND StartAddress &lt; 0x7FFFFFFFFFFF -- User space only\n\nORDER BY Pid, ThreadId<\/pre>\n<p>A more practical approach for thread analysis in Velociraptor is to combine thread start addresses with the VAD information from Query 1, cross-referencing each thread&#8217;s start address against the list of anonymous executable regions found earlier. Any thread whose start address falls within a VAD region that has no file backing is executing injected code.<\/p>\n<pre>-- Query 4b: Cross-reference thread start addresses with anonymous VAD regions\n-- This version correlates thread execution with the suspicious VADs from Query 1\n\nLET suspicious_vads = SELECT Pid, VAD.Start AS Start, VAD.End AS End\nFROM foreach(\n row={SELECT Pid FROM pslist() WHERE Pid &gt; 4},\n query={\n SELECT Pid, VAD FROM vad(pid=Pid)\n WHERE VAD.Protection =~ \"EXECUTE\"\n AND VAD.Type = \"PRIVATE\"\n AND (VAD.FileObject = NULL OR VAD.FileObject = \"\")\n AND (VAD.End - VAD.Start) &gt; 4096\n }\n)\n\nSELECT\n t.Pid,\n t.Name AS ProcessName,\n t.ThreadId,\n format(format=\"0x%016X\", args=t.StartAddress) AS StartAddress,\n v.Start AS VADStart,\n v.End AS VADEnd,\n \"THREAD IN ANONYMOUS EXECUTABLE REGION\" AS Finding\n\nFROM foreach(\n row={SELECT Pid, Name FROM pslist() WHERE Pid &gt; 4},\n query={\n SELECT Pid, Name, ThreadId, StartAddress AS StartAddress\n FROM threads(pid=Pid)\n WHERE StartAddress &gt; 0\n }\n) AS t\n\nJOIN suspicious_vads AS v ON (\n t.Pid = v.Pid\n AND t.StartAddress &gt;= v.Start\n AND t.StartAddress &lt; v.End\n)<\/pre>\n<p>Results from this query are the highest-confidence injection indicators in this entire workflow. A running thread with its start address inside an anonymous executable memory region is an attacker&#8217;s code executing right now. These findings warrant immediate containment consideration, not just documentation.<\/p>\n<h3>Building a complete hunt artefact<\/h3>\n<p>Individual VQL queries are useful during an investigation, but for regular proactive hunting you want to package the most useful queries into a reusable Velociraptor artefact. Artefacts are YAML files that define the query, any parameters it accepts, and how to present the results. Here is a complete custom artefact covering the most valuable injection indicators.<\/p>\n<pre>-- Custom Velociraptor artefact definition (save as Custom.Hunt.ProcessInjection.yaml)\n-- Deploy via: Velociraptor GUI &gt; Server Artifacts &gt; Add Artifact\n\nname: Custom.Hunt.ProcessInjection\ndescription: |\n Detects process injection by identifying:\n 1. Executable anonymous VAD regions (shellcode staging)\n 2. PE headers in anonymous memory (reflective DLL injection)\n 3. Active threads executing from anonymous memory regions\n\n High entropy RWX regions are the strongest indicator.\n PE headers in anonymous memory are definitive for reflective loading.\n Active threads in anonymous regions indicate ongoing execution.\n\nauthor: justruss\ntype: CLIENT\nparameters:\n - name: MinRegionSize\n description: Minimum VAD region size in bytes to consider (filter noise)\n default: \"4096\"\n type: int\n - name: ExcludedProcesses\n description: Comma-separated process names to exclude from scanning\n default: \"MsMpEng.exe,SgrmBroker.exe,esrv_svc.exe\"\n - name: ScanAllProcesses\n description: If false, only scans user-created processes (PID &gt; 1000)\n default: \"Y\"\n type: bool\n\nsources:\n - name: AnonymousExecutableRegions\n description: VAD regions that are executable but have no file backing\n query: |\n LET excluded = split(string=ExcludedProcesses, sep=\",\")\n\n SELECT\n Pid,\n Name AS ProcessName,\n Exe AS ProcessPath,\n VAD.Start AS RegionStart,\n format(format=\"0x%016X\", args=VAD.Start) AS RegionStartHex,\n VAD.End AS RegionEnd,\n VAD.Protection AS MemoryProtection,\n format(format=\"%d KB\", args=[(VAD.End - VAD.Start) \/ 1024]) AS RegionSizeKB,\n entropy(string=read_file(\n accessor=\"process\",\n filename=format(format=\"\/%d\/%d\", args=[Pid, VAD.Start]),\n length=min(a=4096, b=(VAD.End - VAD.Start))\n )) AS EntropyScore,\n -- Check for MZ header indicating reflective PE load\n if(condition=read_file(\n accessor=\"process\",\n filename=format(format=\"\/%d\/%d\", args=[Pid, VAD.Start]),\n length=2\n ) = \"MZ\",\n then=\"YES\", else=\"NO\") AS HasPEHeader,\n -- Risk scoring\n if(condition=VAD.Protection = \"EXECUTE_READ_WRITE\",\n then=\"CRITICAL\",\n else=\"HIGH\") AS RiskLevel\n\n FROM foreach(\n row={\n SELECT Pid, Name, Exe FROM pslist()\n WHERE Pid &gt; 4\n AND NOT Name IN excluded\n },\n query={\n SELECT Pid, Name, Exe, VAD FROM vad(pid=Pid)\n WHERE VAD.Protection =~ \"EXECUTE\"\n AND VAD.Type = \"PRIVATE\"\n AND (VAD.FileObject = NULL OR VAD.FileObject = \"\")\n AND (VAD.End - VAD.Start) &gt;= MinRegionSize\n }\n )\n ORDER BY EntropyScore DESC, (VAD.End - VAD.Start) DESC\n\n - name: ActiveThreadsInAnonMemory\n description: Threads currently executing from anonymous memory regions\n query: |\n LET excluded = split(string=ExcludedProcesses, sep=\",\")\n\n LET anon_vads = SELECT Pid, VAD.Start AS VStart, VAD.End AS VEnd\n FROM foreach(\n row={SELECT Pid, Name FROM pslist() WHERE Pid &gt; 4\n AND NOT Name IN excluded},\n query={\n SELECT Pid, VAD FROM vad(pid=Pid)\n WHERE VAD.Protection =~ \"EXECUTE\"\n AND VAD.Type = \"PRIVATE\"\n AND (VAD.FileObject = NULL OR VAD.FileObject = \"\")\n AND (VAD.End - VAD.Start) &gt;= MinRegionSize\n }\n )\n\n SELECT\n t.Pid AS Pid,\n t.Name AS ProcessName,\n t.ThreadId AS ThreadID,\n format(format=\"0x%016X\", args=t.StartAddress) AS ThreadStartAddress,\n format(format=\"0x%016X\", args=v.VStart) AS VADStart,\n format(format=\"0x%016X\", args=v.VEnd) AS VADEnd,\n \"ACTIVE THREAD IN INJECTED REGION\" AS Finding,\n \"CRITICAL\" AS RiskLevel\n\n FROM foreach(\n row={SELECT Pid, Name FROM pslist()\n WHERE Pid &gt; 4 AND NOT Name IN excluded},\n query={\n SELECT Pid, Name, ThreadId, StartAddress\n FROM threads(pid=Pid)\n WHERE StartAddress &gt; 0x1000\n }\n ) AS t\n\n JOIN anon_vads AS v ON (\n t.Pid = v.Pid\n AND t.StartAddress &gt;= v.VStart\n AND t.StartAddress &lt; v.VEnd\n )<\/pre>\n<h3>Running the hunt and interpreting results<\/h3>\n<p>Once the artefact is deployed, create a hunt targeting your endpoint fleet. In the Velociraptor GUI: Hunt Manager &gt; New Hunt &gt; select <code>Custom.Hunt.ProcessInjection<\/code> &gt; configure parameters &gt; Start Hunt.<\/p>\n<p>The results come back per-client and are queryable across the entire fleet. When results start arriving, work through them in priority order.<\/p>\n<p>First, look at the <code>ActiveThreadsInAnonMemory<\/code> source. Any result here means injected code is actively executing on that machine right now. These machines need immediate attention. Note the ProcessName, Pid, and ThreadID, then proceed to containment or deeper investigation while preserving volatile evidence.<\/p>\n<p>Second, look at <code>AnonymousExecutableRegions<\/code> sorted by <code>EntropyScore<\/code> descending and <code>HasPEHeader = YES<\/code>. These are staged or loaded payloads. High-entropy RWX regions with PE headers are the textbook Cobalt Strike or Meterpreter reflective loading signature. Even without an active thread, the payload is loaded and ready.<\/p>\n<p>Third, look at high-entropy RWX regions without PE headers. These are likely shellcode. Lower entropy RWX regions are more likely to be legitimate JIT-compiled code from .NET or JavaScript engines. The entropy score is your first triage filter.<\/p>\n<h3>Dumping suspicious regions for further analysis<\/h3>\n<p>When you find a suspicious region, the next step is to extract the contents for offline analysis. Velociraptor can do this directly without requiring physical access to the machine.<\/p>\n<pre>-- VQL to dump a specific memory region from a process\n-- Replace Pid and RegionStart with values from your hunt results\n\nSELECT\n Pid,\n Name AS ProcessName,\n upload(\n accessor=\"process\",\n -- The path format is \/PID\/StartAddress for process memory access\n file=format(format=\"\/%d\/%d\", args=[Pid, SuspiciousVADStart]),\n name=format(format=\"pid_%d_addr_%016X.bin\", args=[Pid, SuspiciousVADStart])\n ) AS DumpedRegion\n\nFROM foreach(\n row={SELECT Pid, Name FROM pslist() WHERE Pid = TargetPid},\n query={SELECT Pid, Name FROM pslist() WHERE Pid = TargetPid}\n)<\/pre>\n<p>The dumped binary file will appear in the Velociraptor server&#8217;s upload directory and can be downloaded for analysis. Run it through your Yara ruleset to identify the malware family, use strings to extract indicators, and submit the hash to threat intelligence platforms. If you find a Cobalt Strike beacon, run it through <code>python3 -m cobaltstrikeparser<\/code> to extract the C2 configuration including server address and sleep interval.<\/p>\n<h3>Fleet-wide summary view<\/h3>\n<p>After running the hunt, you want a summary view across all endpoints showing which machines have findings and how many. In the Hunt Manager, after the hunt completes, use the Notebook feature to run a summary query across all client results.<\/p>\n<pre>-- Post-hunt summary query (run in Hunt Notebook)\n-- Shows count of suspicious regions per host, sorted by severity\n\nSELECT\n ClientId,\n client_info(client_id=ClientId).os_info.hostname AS Hostname,\n count(items=RiskLevel) AS TotalFindings,\n count(items=if(condition=RiskLevel=\"CRITICAL\", then=1, else=NULL)) AS CriticalFindings,\n count(items=if(condition=HasPEHeader=\"YES\", then=1, else=NULL)) AS PEHeadersInAnon,\n max(items=EntropyScore) AS MaxEntropy\n\nFROM source(\n hunt_id=HuntId,\n artifact=\"Custom.Hunt.ProcessInjection\/AnonymousExecutableRegions\"\n)\nGROUP BY ClientId\nHAVING TotalFindings &gt; 0\nORDER BY CriticalFindings DESC, MaxEntropy DESC<\/pre>\n<p>This gives you a prioritised list of machines to investigate. Any machine with CriticalFindings greater than zero goes to the top of the queue. Machines with PEHeadersInAnon greater than zero are next. Everything else gets reviewed in entropy order.<\/p>\n<h3>Understanding false positives and building exclusions<\/h3>\n<p>No detection query is perfect on first run. The most common legitimate sources of anonymous executable memory that will appear in your results are the .NET CLR JIT compiler (which generates native code in anonymous RWX regions at runtime), V8 and SpiderMonkey JavaScript engines in browsers (same mechanism), some anti-cheat and DRM systems that use similar techniques for legitimate copy protection, and certain hardware vendor drivers that map executable code outside of standard driver regions.<\/p>\n<p>The right approach is not to exclude these wholesale from the query but to understand what they look like in your environment so you can triage quickly. Run the hunt against clean machines first. Document every finding with its process name, region size, entropy score, and whether it has a PE header. This becomes your baseline. Anything in future hunts that deviates from that baseline is worth investigating. Anything that matches the baseline pattern exactly is likely the same legitimate tool.<\/p>\n<p>In Velociraptor, you can codify these exclusions in the artefact parameters or add them as filter conditions in the WHERE clause. The goal is a query that returns zero results on clean machines in your environment and fires reliably when injection occurs.<\/p>\n<h3>Integrating with your investigation workflow<\/h3>\n<p>Process injection detection should be part of a regular hunting cadence, not just a one-time exercise. Schedule the hunt to run weekly across your full endpoint fleet. Configure alerting so that any machine returning findings from the <code>ActiveThreadsInAnonMemory<\/code> source triggers an immediate notification. Build a response playbook that defines exactly what happens when the hunt fires: who gets notified, what evidence is collected, whether the machine is isolated, and what the escalation path is.<\/p>\n<p>The combination of regular proactive hunting with a clear response playbook is what separates a mature security programme from one that is just running tools. The hunt finds things. The playbook determines what you do with what you find.<\/p>\n<p><strong>Continue reading:<\/strong> The follow-up to this post goes deeper into enriching these findings by cross-referencing memory images, Windows event logs, filesystem artefacts, network telemetry, and lateral movement indicators into a complete investigation narrative. <a href=\"http:\/\/justruss.tech\/index.php\/2025\/05\/21\/velociraptor-process-injection-investigation-full-multi-layer-enrichment\/\">Read Part 2: Full Multi-Layer Enrichment<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A complete walkthrough of using Velociraptor VQL to detect process injection by finding anonymous executable memory regions, PE headers loaded without disk backing, and active threads executing injected code. Includes a full reusable hunt artefact.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-298","post","type-post","status-publish","format-standard","hentry","category-dfir"],"_links":{"self":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/298","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/comments?post=298"}],"version-history":[{"count":3,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/298\/revisions"}],"predecessor-version":[{"id":349,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/298\/revisions\/349"}],"wp:attachment":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/media?parent=298"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/categories?post=298"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/tags?post=298"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}