You are here

Process Hollowing with Manalyze's PE library

ivan's picture

For some reason, articles about process injection techniques seem to be popular these days, and I thought it was the perfect opportunity to write something I have had in mind for a long time. As some of you may know, I maintain Manalyze, a static analyzer for PE executables. One key part of this program is obviously its parser, as writing PE parsers is notoriously hard. For this reason, I took great pains to make sure this part of Manalyze could be reused in other projects. A solid documentation exists but examples go a long way and they were sorely missing.

Actual use cases are also a great opportunity to test the API offered by the program and identify what functions might be missing.

So today, let’s use the PE parser to perform Process Hollowing. Prerequisites are familiarity with C++ and, if you intend to compile the code, you should first follow these instructions which will guide you through the steps needed to reuse Manalyze’s code.
Before we dive in, I just want to stress one more time that the code that follows has been developed for demonstration purposes. While it works fine, it is obviously not lightweight (because of the external dependencies) nor stealthy and you would be ill-advised to copy-paste the code into your homemade RAT project. In addition, to make the code snippets in the article as readable as possible, I have excluded all the error checking. You can find the complete source on GitHub if you want the full experience.

Process Hollowing in practice

Process Hollowing is nothing new. It was described in 2010 in the Malware Analyst’s Cookbook, and possibly before. The idea behind this technique is to create a new process, but replace its contents with an arbitrary program before it gets to execute any code. In the eyes of the system (and potential security programs), a perfectly legitimate application was spawned when in fact a malware has been loaded.

This technique does however require quite a bit of handwork because many tasks normally handled by the Windows loader need to be undertaken manually by the attacker.

1. Creating the host process

Contrary to other injection methods which infect existing processes, this one requires the host to be spawned by the attacker – and in suspended state, no less. This is because the hijack takes place just before the host’s entry point is called. A simple   call takes care of this:


The only element worth noticing is the   flag which was discussed above. It states that the process will not run until the   function is called. Let the drawing below represent that new process and its memory:

Process Hollowing with Manalyze's PE library

For those who are not familiar with the   (Process Environment Block), it’s an internal Windows structure which describes the process itself. We don’t need to concern ourselves with it too much right now; let’s just note that it contains the address where the PE is loaded in the memory. As for the PE image, as we will see, it’s basically the same data that is present in the executable file on the disk with slight layout modifications.

2. Finding out where to work

The next step is to copy the new PE image to the host, but first we need to talk about Address Space Layout Randomization (ASLR). ASLR is an exploit mitigation technique which causes compatible programs to be loaded at unpredictable addresses in memory – we’ll see how it works in practice a bit later. In the example above, I chose to place the host’s Image at address   because it’s what compilers usually put in the PE’s ImageBase field. In the Windows XP era, that value was usually respected but in the modern age where most programs are ASLR compatible, it is ignored and the images get loaded anywhere.

A few questions arise because of ASLR:

  • Where should the new image be written? Ideally, we would like to overwrite the previous image to be as inconspicuous as possible, but we must first make sure that the binary we’re trying to inject is compatible with ASLR. Otherwise, we will only be able to place it at its advertised  .
  • In case we can indeed overwrite the original image, how do we find out where it is located since it’s unpredictable by design?

Checking whether an executable is ASLR-compatible is very easy with Manalyze; here’s how to do it:


First, a PE object is created by providing the parser with a path to the file on the disk. The code has been simplified here, but some light error-checking should take place there to make sure that the input file is valid. The flag which describes whether a PE is compatible with ASLR is located in the   field of the   structure. Accessing it with Manalyze is quite straightforward, and in the real world we would simply compute   to check whether the result is zero. Here, I chose to highlight a function provided by the library to translate a list of flags into a vector of strings ( ). The first parameter is the value to translate, and the second one is the dictionary to use. Manalyze comes with most of the lookup tables you’ll need. While it causes some unnecessary overhead, the function can prove useful when the Windows macros are unavailable or for display purposes.

Secondly, to figure out where the host process has been loaded in memory, we need to resort to Windows trickery. In a classic shellcode context, we would access   directly, but this is not something we can do remotely. When the target is paused because of the creation in suspended mode however, the   register contains a pointer to the  , and that structure possesses an undocumented field (at offset 8) which is the base address of the loaded image. The following code snippet retrieves this value (error handling omitted, no Manalyze code involved):


For the rest of this article, we will assume that both executables are ASLR compatible (as is usually the case), and that the new PE image is to be placed exactly where the original one was placed. The code posted on GitHub handles the other cases too.

3. Writing the PE image

Overwriting the previous PE image directly is risky: what if the allocated space is insufficient for the new one? It’s safer to first release the memory region and allocate it again with the right size and permissions. This can be done through the (undocumented, again)   function that needs to be resolved at runtime through the classic   /   combo. A call to   allows us to obtain the memory region again:


At this point, the memory looks like this:

Process Hollowing with Manalyze's PE library

The image is composed of multiple parts (“sections”) which all need to be copied. The first of them is the PE header:


Manalyze provides direct access to the PE’s bytes through  . Here, we’re only interested in the various PE headers which are located at the beginning of the file, so all we need are the first   bytes which we write to the remote process immediately.

Then, each section of the PE (as described in the file headers) need to be copied as well:


Manalyze’s PE library provides simple access to the sections through  , which returns a vector to iterate on. For each of them, the following needs to be done:

  1. Determine the address at which the section is to be placed. In the PE specification, there is a distinction between offsets in the actual PE file (i.e. the .exe on your disk) and relative virtual addresses (RVA). If the RVA for a given section is   and the address at which the PE was loaded is  , then the section should be mapped at  . The way the RVA is obtained should be self-explanatory. RVAs and file offsets do not correspond, so if you need to translate, Manalyze provides an  .
  2. The section bytes must be written in the remote process. As for the PE before, section objects offer a   function.
  3. Finally, the memory region’s permissions were set to read-write when the section was created. Each section has “characteristics” flags which indicate what permissions are required. While the memory could have been marked as readable, writable and executable from the start and left as is,   sections are a telltale sign of shenanigans. Best restore the permissions as they were initially intended. In this snippet, the   (not from Manalyze’s API, but implemented in this program) is a simple lookup table which translates section characteristics into the correct Windows constant (i.e.  ).

While I won’t paste the code, the permissions of the PE header that was copied at the beginning will need to be updated to   as well (but later, as we’ll need to modify it in subsequent steps). Let’s look at the new memory layout:

Process Hollowing with Manalyze's PE library

Looks good! You’ll notice that I updated the drawing by showing that   points to the PEB. I didn’t mention it before, but   is important too: it points to the program’s entry point.

4. Updating the entry point

Even though the new PE was mapped at the same place as the previous one, entry points differ between executables (i.e. the   function isn’t always mapped at the same virtual address). It is therefore extremely likely that   points to uninitialized or random data, so it should be changed too:


The desired entry point address can be found in the  , and the value of   is changed with a call to  . Had we decided to map our new image somewhere else in memory, this would also be a good opportunity to update the  ’s  , but this is not necessary this time as we use the same base as the host.

5. Surviving “ASLR”

In terms of mapping, we’re done. Had we placed the injected PE at its preferred base, we would be done; however, if you try running the code at this point, you’ll notice the program crashes painfully. Looking at the assembly listing of the executable, you’ll notice that some instructions refer to absolute addresses in memory, i.e.  . Obviously, as the program was not based at   (unless you’re extremely lucky), the injected PE cannot run properly. It needs to be “relocated”. If you recall, I mentioned ASLR earlier. Because PE files are liable to be mapped at random addresses, a mechanism was introduced to fix all those absolute references at load time.

The   section of the PE contains a list of addresses that need to be corrected after the OS has determined the actual  . Usually, that’s the loader’s job but we need to replace it here:


What this function does is calculate the difference between the expected image base and the actual one (delta), then iterates on each relocation to find out all the addresses that need to be edited. I’m not getting into the details of the  , as all it does is:

  1. Read the remote value located at target_address ( ).
  2. Add delta to this value.
  3. Write the new value in the host process ( ).

It would of course have been possible to patch the injected PE before writing it into the remote process, but it was simpler to work with virtual addresses for the purpose of this discussion.
Finally, one last value has to be rebased: the   in the PE headers. You’ll notice that the offset is calculated manually, which is something I’m not happy about. I’ll talk more about this in the conclusion.

6. Resume the execution

The “hollowing” is completed! The only thing that’s left, besides closing open handles and the usual cleanup is to resume the execution of the host program:


Let’s have a final look at the memory at this exact instant:

Process Hollowing with Manalyze's PE library

Hopefully, the injected process will now run and you’ll get the expected results. The animated GIF below shows   being run inside  :

Process Hollowing with Manalyze's PE library


For a 2010 technique, implementing process hollowing turned out to be much harder than I expected. The main difficulty I faced was debugging issues, as forgetting anything results in a generic fatal error. While there is ample documentation regarding the basic steps to follow, I didn’t find any source detailing the rebasing process save this one.

The code has been tested on Windows 7 only. I have no idea whether it works on more recent versions. I’ve also seen allusions to the possibility of injecting x86 executables into x64 processes (and vice versa), but I’m not jumping down that rabbit hole.

All in all, this was a learning experience and I’m glad I managed to write this code with close to no PE parsing, solely relying on Manalyze’s PE library. It’s been a great occasion to see how usable it is, but also where its weaknesses lie. In particular, I observe that while Manalyze is very good at obtaining information from PE files, the API is really lackluster when it comes to modifying them. I can offer an explanation: to offer a unified interface to manipulate both x86 and x64 executables, Manalyze adds an abstraction layer which takes a little distance with the PE specification. Translating data back is definitely possible, but requires some code that doesn’t exist at the moment.

Writing something like:


…would be a lot better of course, but at the same time there are many features I want to implement and I can only devote so much time to the project. For the time being, I will keep prioritizing defensive features (those pertaining to program analysis) over offensive ones (packer generation, etc.).

If you’re interested in obtaining as much information as possible on a PE file however, I still believe that Manalyze’s parser can prove useful to you.