Skip to content

Lesson 7.5 · Malware Behaviours· 50 min

Understanding Process Injection

The process injection families, the memory and telemetry artefacts each one leaves, and how cross-view tools like pe-sieve and Volatility prove one occurred.

Objectives

  • Name the process injection families and explain what each one changes about a target process
  • Read the primitives a family relies on — a remote allocation, a queued APC, a suspended thread's redirected context — from imports, calls or Sysmon events
  • Distinguish 'code is running somewhere it shouldn't' from 'a module is missing from the list that should show it', and pick the right check for each
  • Apply cross-view detection with pe-sieve, hollows_hunter, Moneta and Volatility 3 to prove that injected code exists, independent of what the process reports about itself

Credential Theft and Keylogging looked at what malware does once it is running. This lesson looks at where it runs. Process injection is the general name for getting code to execute inside a process that did not load it — usually a legitimate one, so that whatever the code does inherits that process's identity, network reputation and place in the process tree. Processes, Threads and DLLs already flagged the single most useful observable for this — a thread whose start address resolves to no module at all — and touched several injection techniques as PEB- and handle-level artefacts. This lesson goes one level deeper: what each family actually does inside the target, and how a defender proves it happened even when the process's own APIs have nothing useful to say. Hooking and User-Mode Rootkits picks up immediately afterwards, applying the same cross-view method to code that patches functions rather than code that arrives from nowhere.

Process injection is ATT&CK technique T1055, with a long list of sub-techniques. This lesson organises them by mechanism rather than by ID, because the mechanism is what determines what you can see.

Why processes, not files

A payload that never touches disk as an executable, and that runs inside explorer.exe or a browser instead of its own process, is harder to triage in three specific ways:

  • File-based tools see nothing. Antivirus signature scanning, hash reputation and static PE analysis all assume there is a file to scan. Injected code may exist only in memory.
  • Process-based attribution breaks. Network connections, file writes and child processes are recorded against the host process. A connection from explorer.exe to an unfamiliar IP looks, to most telemetry, exactly like a connection from Explorer.
  • The target process may already be trusted. Security tooling that allow-lists signed system binaries, or an analyst who filters a process tree down to "anything unusual," both walk straight past a legitimate image hosting someone else's code.

None of this requires a kernel exploit or an unpatched vulnerability. Every technique below uses documented, exported Windows APIs, called from an ordinary user-mode process with the rights its token already grants against another process it is allowed to open. That is why detection here is about recognising combinations and consequences of ordinary calls, not about spotting a single suspicious import.

The families

Group every technique by what it does to memory and control flow, and the right detection follows almost automatically.

FamilyCore ideaTypical primitives
Remote allocate-write-executePut code in a target's address space, then make a thread run itOpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread
Reflective loadingLoad a full PE (usually a DLL) without ever calling LoadLibrary on a fileA hand-written loader that resolves imports and applies relocations itself
Process hollowingStart a legitimate process suspended, discard its original image, put another one in its placeCreateProcess(..., CREATE_SUSPENDED, ...), NtUnmapViewOfSection, WriteProcessMemory, SetThreadContext, ResumeThread
Process doppelganging / ghostingGet the loader to map a payload from a file that is deleted, or never fully visible, before executionTransacted NTFS (doppelganging) or a delete-pending handle (ghosting), then a section created from that file
APC injectionQueue code to run the next time a target thread enters an alertable waitOpenThread, QueueUserAPC; early-bird queues it to a suspended thread of a freshly created process, before the process has run any of its own code
Thread execution hijackingRedirect a thread that already exists, instead of creating oneSuspendThread, GetThreadContext/SetThreadContext, ResumeThread
Section-mapping injectionShare memory between processes with a section object instead of WriteProcessMemoryNtCreateSection, NtMapViewOfSection in both the source and the target
Module stompingLoad a real, signed DLL, then overwrite its mapped code with the payloadLoadLibrary on a legitimate DLL, then VirtualProtect + a direct memory write into that module's own image region
Atom bombingSmuggle a payload into the global atom table, then use APCs to a NtQueueApcThread-reachable function to copy it out and execute itGlobalAddAtom/GlobalGetAtomName, QueueUserAPC targeting NtQueueApcThread

Two of these deserve a closer look, because they change what an analyst should even expect a "clean" answer to look like.

Process hollowing: the process lies about its own image

Process hollowing starts a legitimate binary — often one that the SOC already trusts, such as svchost.exe or a signed vendor binary — suspended, so its single thread has not executed a single instruction. The injector then:

  1. Reads the suspended process's PEB to find its image base (or, more reliably, decides on a base of its own).
  2. Calls NtUnmapViewOfSection to discard the original, verified image.
  3. Writes the payload's headers and sections into that address range with WriteProcessMemory, applying relocations if the new image did not land at its preferred base.
  4. Points the thread's context (SetThreadContext, adjusting Rcx/Rip depending on architecture) at the payload's entry point.
  5. Calls ResumeThread.

What makes this different from remote allocate-write-execute is that the legitimate code is gone. The process's own path, command line and even code signature checks performed at launch time were all correct — for a binary that is no longer running. Everything after step 2 belongs to the payload.

Doppelganging and ghosting: the file was never really there

Process doppelganging abuses Transactional NTFS: write the payload to a file inside an NTFS transaction, create a section from that file, map it into a new process, then roll back the transaction. The file never commits to disk in a way ordinary tools can read, yet a mapped, executing image now exists. Process ghosting achieves a similar end differently: create a file, mark it for deletion (FILE_DELETE_ON_CLOSE / FileDispositionInfo), write the payload, create the image section from the still-open-but-delete-pending handle, then let the delete complete before the process is fully created. Antivirus scanning that runs against a file path, rather than the section/handle that was actually mapped, can miss the payload in both cases because by the time anyone looks, there is no file left to scan.

The shared lesson: the file system and the in-memory image are two different sources of truth, and both of these families exist specifically to make the first one lie.

What you can actually observe

Every family above ultimately does one of two things to the target, and each has its own detection surface.

Memory that should not be there

Remote allocation, hollowing, doppelganging/ghosting and section-mapping injection all leave executable memory not backed by the file that the process's own module list says is running there — the same VAD-versus-PEB mismatch that Processes, Threads and DLLs introduced. System Informer's Memory tab and Volatility's memory-map plugins both classify every region as Image (mapped by the loader from a file that matches), Mapped (a section, possibly shared, possibly matching a file elsewhere) or Private (anonymous, VirtualAlloc-style memory with no file behind it at all). The questions that matter:

QuestionWhat it tells you
Is this region Private and executable (PAGE_EXECUTE_READWRITE or a two-step PAGE_READWRITE → PAGE_EXECUTE_READ)?Legitimate code is rarely both writable and executable at once; JIT engines are the main exception, and they are identifiable by which process they run in
Does this region start with MZ, or contain recognisable PE section names in its middle?A full image mapped somewhere the loader does not know about — hollowing, doppelganging, ghosting, reflective loading
Does an Image-type region's on-disk file actually match what is mapped?A stomped module: the file on disk is genuine, but the mapped bytes were overwritten after load
Does any thread's start address fall inside a region with no matching module?The execution trigger for whatever is sitting in that memory

Control-flow primitives that do not match normal use

The other observable is how execution was redirected, independent of where the code sits:

  • CreateRemoteThread / NtCreateThreadEx targeting another process is Sysmon Event ID 8, with SourceImage, TargetImage, StartAddress, StartModule and StartFunction. A blank StartModule on a remote thread is the single strongest one-line signal this lesson has to offer.
  • OpenProcess followed by memory-write rights is Sysmon Event ID 10, the same access-mask logic from the credential-theft lesson — here the target is not lsass.exe but an arbitrary process, and the rights to watch for are PROCESS_VM_WRITE (0x0020) and PROCESS_VM_OPERATION (0x0008) alongside PROCESS_CREATE_THREAD (0x0002).
  • Hollowing and doppelganging/ghosting are specifically what Sysmon Event ID 25 ("ProcessTampering", change types Image is replaced and Process is doppelganged) was added to catch, because Event 8 and 10 alone do not capture "the image this PID reports is not the image that ran."
  • APC injection has no dedicated Sysmon event as of current releases; QueueUserAPC/NtQueueApcThread calls are visible through ETW-based EDR telemetry or user-mode API hooking (see the next lesson), and indirectly through the process-access event that obtained the thread handle in the first place.
  • Thread execution hijacking leaves a thread whose start address is legitimate (nothing to see there) but whose current context, read live or from a memory image, points somewhere the start address does not explain. Compare a thread's recorded start address with where it is actually executing right now; a mismatch between the two is the tell, not either value alone.

Cross-view detection, the general method

None of the observations above can be trusted from inside the affected process, because a sufficiently capable payload can hide from APIs that process itself would answer. The reliable pattern, shared with the next lesson's treatment of hooks, is cross-view detection: ask the same question from a vantage point the payload does not control, and treat any disagreement as a lead.

ToolVantage pointWhat it reports
pe-sieveOne live process, from outside itEvery memory region compared against what it should be: unbacked executable regions, modules whose in-memory code differs from disk, a hollowed or hooked module reconstructed and dumped
hollows_hunterEvery running processRuns pe-sieve's engine system-wide in one pass — the practical way to sweep a host rather than a single suspect PID
MonetaMemory regions across the system, IOC-focusedFlags private executable memory, image regions with modified code, and threads whose start address does not resolve to a mapped, matching module
Volatility 3An acquired memory imagewindows.malware.hollowprocesses and windows.malware.psxview-family plugins compare the process list against pool scanning; windows.vadinfo/windows.memmap expose region types directly; windows.malware.threads (and equivalent scanning) surfaces threads with no matching start module

The shape of the argument is always the same: a live, high-level view (what the process's own loader data structures report) versus a lower-level view (what memory actually contains, or what a scan of every process finds). Where they disagree, the lower-level view wins, because the mechanisms in this lesson exist specifically to make the high-level view wrong.

Tip: pe-sieve and hollows_hunter cannot tell you why a region is unbacked — a legitimate JIT engine and a hollowed process both produce private executable memory. Feed the finding into the same classification questions the next lesson uses for hooks: what backs the destination, is it signed, does a clean baseline show the same thing.

Reporting an injection finding

Match the family to the artefact, and state both, because "process injection occurred" is not by itself actionable:

FieldExample
Family and ATT&CK sub-techniqueProcess hollowing, T1055.012
HostSource PID/image, target PID/image, whether the target still exists
EvidenceSysmon 8/10/25 event(s); pe-sieve or Volatility output; memory region type and permissions
What the process claims vs. what is trueReported image path/signature at launch vs. what is actually mapped now
PayloadDumped module hash, if recovered; entry point; whether it is itself packed
Detection gapWhich of your existing tools would have missed this, and why

Lab: baseline "normal" memory before you go looking for injection

This lab builds the reference you need before any injection finding makes sense: what ordinary processes' memory regions look like, so that an anomalous one stands out. Nothing here injects into anything; every process you inspect is one you started yourself, and pe-sieve is run in its default, read-only scanning mode. Do this inside your analysis lab.

  1. Install the tools on your Windows analysis VM: download pe-sieve64.exe and hollows_hunter64.exe (hasherezade's releases), and Moneta, alongside System Informer, which FLARE-VM already includes.

  2. Baseline a clean, ordinary process. Start notepad.exe. In System Informer, open it and go to the Memory tab. Sort by Type and note how many regions are Image (the executable and its DLLs), how many are Mapped (fonts, shared sections) and how many are Private. Confirm that none of the executable regions are also writable.

  3. Scan it with pe-sieve while it is doing nothing suspicious at all:

    powershell
    pe-sieve64.exe /pid <notepad_pid>

    Read the summary: on an unmodified process this reports zero suspicious or replaced modules, and zero implants. This is the expected result — record it, because it is what tells you a later finding is unusual rather than normal noise from this tool on this host.

  4. Sweep the whole system with hollows_hunter64.exe /hooks (hooks scanning on, so you also see what your antivirus or EDR legitimately patches). Expect several processes to show inline hooks from security software — that is a hooking finding for the next lesson, not an injection finding for this one; the distinction is exactly the "family" table above.

  5. Read a thread's start address deliberately. In System Informer, open any ordinary process (e.g. explorer.exe), go to Threads, and for two or three threads note the Start Address column resolving to module!function. This is the negative case for the "blank start module" signal: confirm you can find it and recognise what "normal" looks like before you ever need to spot its absence.

  6. Correlate with Sysmon. With Sysmon running and configured for events 1, 8 and 10, open any two of your own benign programs and have one open a handle to the other with OpenProcess (for example, reuse the lab.exe from the Processes, Threads and DLLs lesson, which already keeps a process handle open to its cmd.exe child). Find the corresponding Event ID 10 and read its GrantedAccess mask with the same bit-by-bit method from the credential-theft lesson. Confirm it carries no memory-write rights — this is what a benign handle looks like, next to the credential-theft lesson's worked example of a malicious one.

Questions to answer: Which of the Private regions in your Notepad baseline are actually normal (think about JIT, ASLR working set, or the clipboard), and how would you tell one of those apart from an injected payload if you saw it in an incident? Why does pe-sieve's "zero implants" result on a healthy process matter as much as a positive result on an infected one? If a hollowed process's original image was a signed Microsoft binary, what in this lab's memory-type check would still reveal that the signature no longer means anything?

Key takeaways

  • Process injection is grouped by mechanism, not by name: remote allocate-write-execute, reflective loading, hollowing, doppelganging/ghosting, APC-based (including early-bird), thread hijacking, section mapping, module stomping and atom bombing each change memory or control flow differently, and each has a different observable.
  • The two general artefacts are executable memory that does not match what the module list or file system claims is running, and control-flow primitives (remote threads, hijacked contexts, queued APCs) used against a process the caller does not own.
  • Sysmon Event 8 (remote thread, watch for a blank start module), Event 10 (process access, test the mask bits) and Event 25 (ProcessTampering) cover most of this ground; APC injection currently has no dedicated event and needs API-level telemetry.
  • Trust nothing the target process reports about itself. Cross-view tools — pe-sieve, hollows_hunter, Moneta, Volatility 3 — compare a live or acquired low-level view of memory against the high-level view an API or a module list would give you, and treat any disagreement as the finding.
  • Always establish what "clean" looks like — memory region types, thread start addresses, a benign process-access mask — on the same tooling before an incident, so an anomaly is recognisable as one.