Developing PE file packer step-by-step. Step 2. Packing

Previous step is here.

Straight off I want to say that as I write this series of articles I fix some things and update my PE library (Note, that this step is for 0.1.x versions, too).

And we continue to develop our own packer. At this step it is time to turn directly to PE file packing. I shared a simple packer long time ago, which was ineffective by two reasons: firstly, it uses standard Windows functions for data packing and unpacking, which are rather slow and have low compression rate, secondly, all PE file sections were packed individually, which is not very optimal. This time I will do this differently. We are going to read data of all sections at once, assemble them into one block and pack. So, the resulting file will have only one section (actually two, I will explain this later), we can place all the resources, the packer code and helper tables into it. This will provide some benefits, because we don't need to spend space for file alignment, besides that, LZO algorithm is much more effective than RtlCompressBuffer in all respects.

Therefore, packer operation algorithm will be roughly the following one: read all sections, copy their data to one buffer and pack it, place the packed buffer to a new section, delete all remaining sections. We will have to store all original file sections parameters, to let unpacker restore them later. Let's write a specific structure for this:

This structure will be placed to specific offset in packed file for each section, and packer code will read these structures. They will store all required information to restore PE file sections.

Besides that, it will be handy to have a structure to store various useful original file information, which will also be necessary for unpacker. It will have three fields only for now, and most possibly I will expand it in time:

Please pay your attention that both structures have alignment 1. This is required to reduce their size. Besides that, setting the alignment size obviously will save you from various issues with reading structures from file during its unpacking.

Let's go further. It is good to calculate file sections entropy before packing, to determine if it is reasonable to pack it or it was already packed. My library allows to do that. Also, it is worth to check whether we have .NET binary file - we will not pack such files.

Now let's turn to sections packing. Let's add #include <string> line to the beginning of main.cpp - we will need strings to form data blocks (they place data sequentially, and we can write them to file directly from the string). We may also use vectors (vector), however, there is no big difference.

To begin we should initialize LZO library at first:

Read file sections:

Turn to file packing:

I will explain the code above a little. We created two buffers - packed_sections_info and raw_section_data. Ignore the fact that these are strings (std::string), they can store binary data. First buffer stores sequential packed_section structures, which are created and filled for all sections in PE file. Second buffer stores all sections raw data assembled together. We will be able to split and put this data to sections again after unpacking, because the information about section raw data sizes is stored in the first buffer and will be available for unpacker. Then we go further - we need to pack resulting raw_section_data buffer. We can pack packed_sections_info buffer with it - well, let's do this. We concatenate strings (in fact, binary buffers) packed_sections_info and raw_section_data - this is performed in previous code block.

Further we will create a new PE file section to place our packed data:

So, we created a new section (but have not added it to PE file yet). Why did I name it .rsrc? I did it for one simple reason. All files have their resources in section named .rsrc. Main file icon and its version information are also stored in resources. Unfortunately, Windows explorer can read file icon and display it ONLY if section with resources is named .rsrc. As far as I know this issue was fixed in later Windows versions and service packs, but it is better to get reinsured. We do not work with resources so far, so this is done for the future.

Data compression is the next step. Slightly low-level part... Here we will need Boost library. Don't you have it? It's time to download, build and install it! This is very easy. But for the library class, which I am going to use further, there is no need to build it. Just download the library, unpack it to some folder, for example, C:\boost, and put the path to boost header files to project include directories. If I will need boost class later, which should be built, I will explain how to do that.

Let's add #include <boost/scoped_array.hpp> line to main.cpp headers. Then we will pack the data.

Now we have to delete unnecessary PE file sections and add our new section to it:

What happened here? I will explain it in more detail. At first we determined virtual address of first PE file section (see this below). After that we determined total virtual size of all sections. As section virtual size equals virtual address + aligned virtual size of previous one, then, having virtual address and size of last file section, we got total virtual size of all sections plus first section address. We get pure virtual size of all sections together by subtracting that first section address from this number. This can be performed easier - by calling image.get_size_of_image() function, which would return, in fact, the same, but from PE file header, but well. Further we delete all existing PE file sections. After that we add our section to PE file and get a reference to added section with recalculated addresses and sizes (we work with this reference after adding). Then we should reserve enough memory to unpack all sections into it - that's why we change newly added section virtual size to total size of all existing sections. Added section virtual size will be calculated automatically by default. This doesn't fully fit our requirements - we need that memory region occupied with our section should totally match the region occupied by all original file sections. My library allows to set section virtual address explicitly, if this section is first in file (i.e. there was no other sections before this one was added). This is our situation actually. This is the reason why we determined virtual address of first section and set it for our new section.

We also changed file alignment to minimum allowed value for aligned files, while file did not have any sections, to make everything go faster.

However, one section is not enough and we have to create and add another one. You might ask: "What for?" The answer is simple: first section after its unpacking will contain all original file sections data. And we still have to place unpacker somewhere. You might say: so, place it to the end of the section. But then it will be rewritten by original file data during unpacking! You may, of course, really place it to the same section, and allocate memory (with VirtualAlloc or somehow else) right before unpacking itself and copy unpacker body there, and run it from that memory. But this memory has to be released somehow. If we do this from itself, application will crash: memory is released, and EIP processor register, which points to currently executing assembler command, points to nowhere. Thus, we can't do without additional section. If you look at UPX or Upack, you will see that they have 2 to 3 sections too.

Let's turn to next step. We will mock at PE file a little:

I deleted almost all more or less usable directories from headers. This is completely wrong, because most files will stop working after that. But you understand, that we are improving the packer step-by-step, so let it be this way for now. I left imports directory only, and I did not handle it in any way. Imports are the first ones, which we have to manage properly, because it is very hard to find a file without imports, and we have to test our packer on something.

Further I stripped the directory table, and because all of directories are deleted now, I removed stub from the header (usually there is DOS stub and Rich MSVC++ signatures, we don't need this). We strip directory table down to 12 elements, not less. Elements from 1 to 12 may be present in original file and we have to restore them. Of course, we could leave absolute minimum of elements in the table, but this will not give benefits in size, and add more code to unpacker, if we will suddenly need to expand the table back. Why do we have to cut the table exactly to 12 elements? That's because last four are definitely not required to launch PE file successfully, and we can manage without them easily. We can also check dynamically if the file has 12th (Configuration directory), 11th (TLS directory) and so on directories, and if not, to strip directory table even more, but, I repeat, there is no big reason in this.

Last thing for us to do is to save the packed file under a new name:

Nothing complicated happens in this code part, everything has to be more or less understandable from comments. So, this is all we do at this step. The step is more than rich, and you have something to think about. Obviously, the packed file will not be loaded, because it doesn't have any unpacker, we don't handle imports and don't fix entry point and many more... However, we can estimate compression rate and check, if everything is packed in the intended way, using any PE file viewer (I use CFF Explorer).

Original file:

Packed file:

As you can see, first section Virtual Address + Virtual Size in the second screenshot matches SizeOfImage in the first one. First section virtual address was not changed. This is what we wanted to achieve. On the second page you can see the contents of second "coderpub" section. Compression rate is not bad - from 1266 kb to 362 kb.

See you at the next step! Questions are welcomed, you can ask them in comments.
And, as always, I share actual project version with latest changes: own PE packer step 2.

Leave a Reply

Your email address will not be published. Required fields are marked *