Writing a PE packer – Part 3 : packing with python

We have everything ready to do the actual packing of an ASLR enabled PE32 file. We’ll turn our loader in an unpacking stub, and use python to create a packed binary.

The unpacking stub

General idea

Our laoder can read a PE file from anywhere, load it into memory and run its content. We going to modify the C code to read the PE file from within a section of the unpacker, named “.packed”. Here is what we are aiming for:

  1. List the sections of the current running process (the unpacker)
  2. Find the “.packed” section : this is a PE file content
  3. Load this PE into memory and execute it.

Modifying the code

We only need to change the main function. To simplify the resulting PE (Mingw32 produce A LOT of sections), we’ll use compilation options to avoid linking with the C standard library and runtime. So we won’t have a main function, but a _start one:

int _start(void) { //Entrypoint for the program

    // Get the current module VA (ie PE header addr)
    char* unpacker_VA = (char*) GetModuleHandleA(NULL);

    // get to the section header
    IMAGE_DOS_HEADER* p_DOS_HDR  = (IMAGE_DOS_HEADER*) unpacker_VA;
    IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) (((char*) p_DOS_HDR) + p_DOS_HDR->e_lfanew);
    IMAGE_SECTION_HEADER* sections = (IMAGE_SECTION_HEADER*) (p_NT_HDR + 1);

    char* packed_PE = NULL;
    char packed_section_name[] = ".packed";

    // search for the ".packed" section
    for(int i=0; i<p_NT_HDR->FileHeader.NumberOfSections; ++i) {
        if (mystrcmp(sections[i].Name, packed_section_name)) {
            packed_PE = unpacker_VA + sections[i].VirtualAddress;
            break;
        }
    }

    //load the data located at the .packed section
    if(packed_PE != NULL) {
        void (*packed_entry_point)(void) = (void(*)()) load_PE(packed_PE);
        packed_entry_point();
    }
}

AS you can see we parse the current module PE header, as we have in our loader, to search for a section named “.packed”, load and run it as a PE file.
As we are not linking with the C library (which we don’t really need), you should remove the stdio.h and stdlib.h includes. As we used strcmp, memcpy and memset, we’ll need to write them back in. I prefixed my versions with “my”, and you can see in the code above that I called mystrcmp instead of strcmp.

Compilation options

Once everything is set in our unpacker, we can compile it with the following options:

mingw32-gcc.exe unpack.c -o unpacker.exe "-Wl,--entry=__start" -nostartfiles -nostdlib -lkernel32

A few words on the options used here:

  • nostartfiles removes the C runtime, the code that actually calls main with the classic argc and argv parameters. In the Winddows world, the OS doesn’t parse the command line. To get each argument, a program has to call the GetCommandLine function and split the result. This is one of the things the C runtime does for us, that we don’t need here.
  • nostdlib : doesn’t link with the standard libraries (libC, kernel32.dll, user32.dll, etc …). We’ll need to tell the linker every library it needs, hence the -lkernel32 option.
  • -Wl,--entry=__start : the entrypoint of our program. This is not the C runtime _start function, running our main one anymore, so we need to tell the linker. Notice there are 2 underscore characters, one more than the function name we used in the code.

That should do it : you should get an unpacker.exe file, ready to be used for packing. If you look at its imports you should only see kernel32.dll, with GetModuleHandleA, VirtualAlloc, VirtualProtect, LoadlibraryA and GetProcAddress functions.

Now, we’re going to add the “.packed” section to this binary, and we’ll be done !

Packing with python

Basic program

This is exactly the kind of little thing that I love to do in python. Nice, simple and easy.
We’re going to use the library called “lief” for handling the PE files. There are others, but I find this one better to write PE files: it computes a lot of fields for us, and has very usefull functions to modify a PE, like adding an import for example. It also has a good documentation, here. And you can get it with pip, so that’s really easy to install.

What we need to do here is very simple: just add a section to the unpacked.exe we compiled before, named “.packed”, and containing a copy of any PE32 file, like calc.exe.

To avoid warnings, we’re going to need those 2 python functions:

def align(x, al):
    """ return <x> aligned to <al> """
    if x % al == 0:
        return x
    else:
        return x - (x % al) + al

def pad_data(data, al):
    """ return <data> padded with 0 to a size aligned with <al> """
    return data + ([0] * (align(len(data), al) - len(data)))

The first one aligns an int, the second one adds padding to data to align its size. Lief does it for us, but raises a warning, so let’s do it ourselves!

So first, let’s take some command line arguments, because that’s so easy in python:

parser = argparse.ArgumentParser(description='Pack PE binary')
parser.add_argument('input', metavar="FILE", help='input file')
parser.add_argument('-p', metavar="UNPACKER", help='unpacker .exe', required=True)
parser.add_argument('-o', metavar="FILE", help='output', default="packed.exe")

args = parser.parse_args()

Adding a section to a PE file

Then we open the 2 PE files: the unpacker, which we are going to have Lief parse, and the one we want to pack (just reading its content):

# open the unpack.exe binary
unpack_PE = lief.PE.parse(args.p)

# we're going to keep the same alignment as the ones in unpack_PE,
# because this is the PE we are modifying
file_alignment = unpack_PE.optional_header.file_alignment
section_alignment = unpack_PE.optional_header.section_alignment

# read the whole file to be packed
with open(args.input, "rb") as f:
    input_PE_data = f.read()

We can then create a section:

packed_data = list(input_PE_data) # lief expects a list, not a "bytes" object.
packed_data = pad_data(packed_data, file_alignment) # pad with 0 to align with file alignment (removes a lief warning)

packed_section = lief.PE.Section(".packed")
packed_section.content =  packed_data
packed_section.size = len(packed_data)
packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
                                | lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE
                                | lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
# We don't need to specify a Relative Virtual Address here, lief will just put it at the end, that doesn't matter.
unpack_PE.add_section(packed_section)

Lief does a lot of computing for us, just put the fields you want it to handle to their default value (zero in most cases), and you’re a go. We now just need to save the file:

# remove the SizeOfImage, which should change, as we added a section. Lief will compute this for us.
unpack_PE.optional_header.sizeof_image = 0


# save the resulting PE
if(os.path.exists(args.o)):
    # little trick here : lief emits no warning when it cannot write because the output
    # file is already opened. Using this function ensure we fail in this case (avoid errors).
    os.remove(args.o)

builder = lief.PE.Builder(unpack_PE)
builder.build()
builder.write(args.o)

Final result

And that’s it. Just pack a binary:

python.exe .\packer.py C:\Windows\SysWOW64\calc.exe -p .\unpacker.exe

You can look at its sections:

packed sections

We just added the “.packed” section, the rest is exactly like the unpacker.exe file.

Execute the result, you should see your calc appear!

The final files can be found here: https://github.com/jeremybeaume/packer-tutorial/tree/master/part3.

The question is, is what we just programmed … useful? Well, really, no.

  • It doesn’t reduce the size of the packed binary: we actually added data to it, so the packed file is bigger than the original (the original is completely contained inside).
  • It doesn’t really obfuscate the code. We just changed the imports: the imports we see if we look at packed.exe are the one of our unpacker, so only kernel32.dll. We hid the calc.exe imports, but that’s not so usefull. And it is trivial to get the original PE back: just extract the “.packed” section content.
  • It doesn’t evade antivirus detection: the original PE is in included inside the packed one, without modifications. So, any antivirus signature matching the original PE would still match the packed one.

What we did for now is of no real-life use, but it is easy to modify to suit any other need. Now may I propose we take a look at the DLLCharacteristics of the packed binary:

DLL chacateristics

And there, we have an “issue”: the packed binary cannot be moved. Mingw32 is not capable of generating a relocation table. you could try to pack a binary compiled with Mingw32, it won’t run correctly.
This is not a really common case, but it an interesting exercice, so let’s handle that case in the next tutorial part: Part 4 : packing with no relocation.

Leave a Reply

Your email address will not be published.