Table of Contents
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:
- List the sections of the current running process (the unpacker)
- Find the “.packed” section : this is a PE file content
- 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 classicargc
andargv
parameters. In the Winddows world, the OS doesn’t parse the command line. To get each argument, a program has to call theGetCommandLine
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 ourmain
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:
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 onlykernel32.dll
. We hid thecalc.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:
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.