Leçon 7.5 · Comportements malveillants· 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.
Cette leçon n’est disponible qu’en anglais pour le moment.
Objectifs
- 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.exeto 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.
| Family | Core idea | Typical primitives |
|---|---|---|
| Remote allocate-write-execute | Put code in a target's address space, then make a thread run it | OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread |
| Reflective loading | Load a full PE (usually a DLL) without ever calling LoadLibrary on a file | A hand-written loader that resolves imports and applies relocations itself |
| Process hollowing | Start a legitimate process suspended, discard its original image, put another one in its place | CreateProcess(..., CREATE_SUSPENDED, ...), NtUnmapViewOfSection, WriteProcessMemory, SetThreadContext, ResumeThread |
| Process doppelganging / ghosting | Get the loader to map a payload from a file that is deleted, or never fully visible, before execution | Transacted NTFS (doppelganging) or a delete-pending handle (ghosting), then a section created from that file |
| APC injection | Queue code to run the next time a target thread enters an alertable wait | OpenThread, 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 hijacking | Redirect a thread that already exists, instead of creating one | SuspendThread, GetThreadContext/SetThreadContext, ResumeThread |
| Section-mapping injection | Share memory between processes with a section object instead of WriteProcessMemory | NtCreateSection, NtMapViewOfSection in both the source and the target |
| Module stomping | Load a real, signed DLL, then overwrite its mapped code with the payload | LoadLibrary on a legitimate DLL, then VirtualProtect + a direct memory write into that module's own image region |
| Atom bombing | Smuggle a payload into the global atom table, then use APCs to a NtQueueApcThread-reachable function to copy it out and execute it | GlobalAddAtom/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:
- Reads the suspended process's PEB to find its image base (or, more reliably, decides on a base of its own).
- Calls
NtUnmapViewOfSectionto discard the original, verified image. - 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. - Points the thread's context (
SetThreadContext, adjustingRcx/Ripdepending on architecture) at the payload's entry point. - 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:
| Question | What 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/NtCreateThreadExtargeting another process is Sysmon Event ID 8, withSourceImage,TargetImage,StartAddress,StartModuleandStartFunction. A blankStartModuleon a remote thread is the single strongest one-line signal this lesson has to offer.OpenProcessfollowed by memory-write rights is Sysmon Event ID 10, the same access-mask logic from the credential-theft lesson — here the target is notlsass.exebut an arbitrary process, and the rights to watch for arePROCESS_VM_WRITE(0x0020) andPROCESS_VM_OPERATION(0x0008) alongsidePROCESS_CREATE_THREAD(0x0002).- Hollowing and doppelganging/ghosting are specifically what Sysmon
Event ID 25 ("ProcessTampering", change types
Image is replacedandProcess 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/NtQueueApcThreadcalls 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.
| Tool | Vantage point | What it reports |
|---|---|---|
| pe-sieve | One live process, from outside it | Every 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_hunter | Every running process | Runs pe-sieve's engine system-wide in one pass — the practical way to sweep a host rather than a single suspect PID |
| Moneta | Memory regions across the system, IOC-focused | Flags private executable memory, image regions with modified code, and threads whose start address does not resolve to a mapped, matching module |
| Volatility 3 | An acquired memory image | windows.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:
| Field | Example |
|---|---|
| Family and ATT&CK sub-technique | Process hollowing, T1055.012 |
| Host | Source PID/image, target PID/image, whether the target still exists |
| Evidence | Sysmon 8/10/25 event(s); pe-sieve or Volatility output; memory region type and permissions |
| What the process claims vs. what is true | Reported image path/signature at launch vs. what is actually mapped now |
| Payload | Dumped module hash, if recovered; entry point; whether it is itself packed |
| Detection gap | Which 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.
-
Install the tools on your Windows analysis VM: download
pe-sieve64.exeandhollows_hunter64.exe(hasherezade's releases), and Moneta, alongside System Informer, which FLARE-VM already includes. -
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. -
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.
-
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. -
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 tomodule!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. -
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 thelab.exefrom the Processes, Threads and DLLs lesson, which already keeps a process handle open to itscmd.exechild). Find the corresponding Event ID 10 and read itsGrantedAccessmask 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.