While earning my Cyber Operations degree from Dakota State University, https://dsu.edu/, I found my reverse engineering and malware analysis classes to be the most interesting and challenging. I decided it would be a fun exercise to try to create my own proof of concept malware using the same techniques that I came across in my studies. This was to be a learning opportunity to become more familiar with Windows APIs, the PE file stucture, and to gain more experience using various tools. I chose process hollowing because I found it to be a very clever way to deliver malicious content, and it would fill all the criteria I wanted to cover in this exercise. Let's begin.
What is process hollowing, exactly? There are many references on-line that describe the process in detail, such as, https://attack.mitre.org/techniques/T1055/012/, so I will give a very general description and then you can follow this post to fill in the blanks. It begins by opening a common process, such as firefox.exe, calc.exe, or svchost.exe, to name a few. A common process is used to avoid drawing attention to itself, as opposed to say, an Excel doc running an executable which is highly suspicious, and to possibly evade anti-virus or other security measures. The executable content of this new process is carved out, and in its place, the malicious code is injected and executed. One of the benefits of this technique is, it is a legitimate process so it can hide in plain sight. The process is also created from it's location in the file system, as opposed to a temp file, which also increases the seeming legitimacy of the process. Also, it all happens in memory, so nothing is written to disk, making it even more difficult to detect. Let’s take a look at an example.
Some of the addresses and register values may vary from groupings of screenshots. This is because the program was run several times during the writing of this post, and ASLR will give different values for each instance. This is run on a Windows 10 VM using Visual Studio Code.
First we need to create our target process that will be hollowed out. For this example “svchost.exe” will be the target process. Figure 1 shows the libraries used, along with a declaration of ZwUnmapViewofSection()
, which will be used to hollow the process. Then, in Figure 2, the main()
function begins by initializing two structures, STARTUPINFOA()
, and PROCESS_INFORMATION()
, which are used to help populate the CreateProcessA()
API, which will be used to start our process. Notice that the process is created in the suspended state, which is essential. Detailed information can be found on MSDN for CreateProcessA.
[Fig. 1]
[Fig. 2]
We can set a break-point in a debugger, just after the CreateProcessA function, and look in ProcessHacker to verify that our code launches an svchost.exe process.
Now that we can spawn a target process, we need to open the malicious process. We can use CreateFileA()
to get a handle to the location of our malicious file in the file system, as shown in Fig. 3.
[Fig. 3]
Now that we know where the file is, we can use VirtualAlloc()
to allocate memory for that file. One of the parameters for VirtualAlloc is dwSize
, so, first we need to run GetFileSize()
to get the size of the malicious file, and then we can allocate memory, as shown in Fig. 4. Make sure you give Read/Write permission for the flProtect
member. VirtualAlloc will now return the base address to the newly allocated memory.
[fig. 4]
Now we can use ReadFile()
to read our malicious content using the handle we got from CreateFileA, and write it into our new memory allocation using the pointer we got from VirtualAlloc, then close the source handle, as shown in Fig. 5. Notice you need to declare the variable ReadFile uses to store the number of bytes the operation reads.
[Fig. 5]
Now let’s check to make sure everything is working correctly. Using a debugger we can inspect the memory that has been allocated by VirtualAlloc()
. It’s return value, in register EAX
, will be the base address of the allocation. In Fig. 6 we can see EAX (RAX)
, contains 0x3C0000
. Fig. 7 shows this memory location is empty immediately following our call to VirtualAlloc()
.
[Fig. 6]
[Fig. 7]
Next, we can look at the same memory location after the call to ReadFile(), which should have read our PE file into the specified memory. Figure 8 shows the memory is now populated with data which appears to be a PE file, indicated by the magic number (MZ).
[Fig. 8]
In order to hollow our process we will need to know the base address of our victim process. This is the location in memory where our process begins. We will also need the entry point. This is location where our processes executable code is located. This will need to be changed to reflect the address of our malicious code, so it will be executed when the process runs. Both of these pieces of information can be found from our victim process’ thread context. This can be found by using the GetThreadContext()
API. This API will populate a context structure, that includes registers storing the values we are looking for. Detailed information on the structure can be found here.
We are looking for the values in register EAX
(entry point memory location), and EBX
(PEB memory location, which can be used to find the base address). We can find these values using Windbg. The “~” command shows available threads. The primary thread is “0” which is in the “Suspend” state, as shown in Fig. 9.
[Fig. 9]
Use the “~0s” command to switch to the primary process thread, then we can examine the PEB contents. We can see that the ImageBaseAddress
is 0x970000
in Fig, 10.
[Fig. 10]
If we look at register EBX
in Fig. 11, we see the address 0x30d7000
. This is the address of the PEB. If we look at the contents of that address + 0x8
(offset from Fig. 10), we can see that the base address 0x00f40000
is indeed at this location.
[Fig. 11]
Next we need to get the target process’ thread context, in order to get the register value with its base address. Using GetThreadContext()
will give us a pointer to a context structure which contains the register values that are needed (See Fig. 12). With our context structure pointer (‘c’), we can get the address of the PEB from register EBX
. Adding 0x8
will give us the address that contains the image base address of our target process, as was demonstrated in Fig. 10.
[Fig. 12]
Now we are armed with all the information we need, therefore, we can begin the actual process of hollowing out the executable. First, we get a handle to ntdll.dll which we then use to get the address of ZwUnmapViewOfFile()
. This function will free the memory in our target process which will allow us to write the malicious file in its place. The code snippet in Fig. 13 shows this process.
[Fig. 13]
Let’s look at the memory to verify what is happening here. We can set a breakpoint before the call to unMapViewOfSection as shown in Fig. 14. Our output shows a PID of 1604, and a base address of 0x00580000
. If we look at that memory address we can see the DOS Header. Then, immediately after the call, the memory has been zeroed out, indicating a successful hollowing. (Fig. 15)
[Fig. 14]
[Fig. 15]
In order to write our malicious file into the hollowed process, we will need to use virtualAlloc to make the memory writable and to tell the OS how much space we will need to reserve. In order to use virtualAlloc we need to know the size of the image. This is different than the size of the file we used earlier. When the process is running it typically will not be the same size as the stored file. This can be caused by several factors, one of which is the compression of sections. We can access this information from the file headers. Figure 16 shows the layout of the DOS Header, PE Header, and Optional Header, respectively(Images courtesy of corkami.com). The DOS Header contains e_lfanew
which is the number of bytes it takes to get from the DOS Header to the PE Header. From there, we can get to the Optional Header, which contains SizeOfImage
. The code in Fig. 17 shows how this works.
[Fig. 16]
[Fig. 17]
Now we can use VirtualAllocEx()
like before, however, this time we need to make sure we assign read/write/execute permissions to this memory. (Fig. 18)
[Fig. 18]
Now it’s finally time to write the malicious file into memory. Figure 19 is a diagram of the PE file structure. As you can see, we first need to write the headers, then we can write the sections, which contain all of the executable data.
[Fig. 19]
We use WriteProcessMemory()
to write the headers to memory. Figure 21 shows the IMAGE_NT_HEADER
structure, which contains the OptionalHeader. Figure 22 shows the IMAGE_OPTIONAL_HEADER
structure which contains the SizeOfHeaders
value we need for WriteProcessMemory.
[Fig. 20]
[Fig. 21]
[Fig. 22]
Writing the sections is a little bit tricky. We can use a loop to copy each section. In order to do this, we need the NumberOfSections
, which is located in the IMAGE_FILE_HEADER
structure (Fig. 24), which is in the NT Headers. Now we need to find the first Section Header. If we add e_lfanew
and the size of the headers to the base address, we will be in the first Section Header . Then we use WriteProcessMemory()
again, getting our to and from addresses from the IMAGE_SECTION_HEADER
structure (See Fig. 25 for details). Now we can loop through and write all of the sections by adding the size of each IMAGE_SECTION_HEADER
. Figure 23 shows the corresponding code.
[Fig. 24]
[Fig. 25]
Once the loop is complete, the injection of our malicious code should be complete. Again, we can try to verify this by looking at the memory of our suspended process after the loop completes. In Fig. 26 we can see that once again we have a PE file instead of the zeroed out memory we had after unmapping the process. This is what we are hoping to see.
[Fig. 26]
All that is left to do is change the EAX value in the thread context to reflect the entry point of our newly injected process, which we can also get from the headers, and resume the thread (Fig. 27)
[Fig. 27]
The final proof is to run the program in its entirety, and see what happens. As you can see in Fig. 28 the command line output tells our process has a PID of 2980. If we look in ProcessHacker we can see that our program did spawn an svchost.exe with a matching PID. However, what has been opened is our “malicious” program which is just a dialog box telling us it is malware. We have been successful in our endeavors! This is a very simple example of how process hollowing works. There are many different ways to accomplish this, but this is the method that worked for me. There was a lot of trial and error, but I did learn a lot on the way, which was really what this was all about. Of course, being successful in the end was also very rewarding! I hope this helps to understand how this process works. Happy hacking.
[Fig. 28]
I did not perform image base relocations to patch the relocation table. I tested my code with my "malicious" pop-up as well as with calc.exe and iexplore.exe. Having had positive results with these I didn't proceed with rebasing. I may revisit this at a later time and see if I can successfully rebase the image to get a more complete resolution. For now, I am pretty satisfied with my small success!