Writing a PE packer – Part 5 : simple obfuscation

In this tutorial, we are going to complete our packer with some very basic obfuscation, as a demonstration of the possibilities we have.

A bit of cleaning

Remove unnecessary information

The first thing we are going to do is do a bit more cleaning in the resulting PE.
We already did a fair bit by removing the standard library and runtime, with the options -nostartfiles and -nostdlib. There is still a useless section named .eh_fram, and our resulting PE may still have some symbols inside.
We’re just going to change the compilation command a bit, to add 2 options: -fno-ident and -fno-asynchronous-unwind-tables which will clean the sections, and pass the stip.exe command on the result to remove anything no necessary (always a good thing to do):

def compile_stub(input_cfile, output_exe_file, more_parameters = []):
    cmd = (["mingw32-gcc.exe", input_cfile, "-o", output_exe_file] # Force the ImageBase of the destination PE
        + more_parameters +
        ["-Wl,--entry=__start", # define the entry point
        "-nostartfiles", "-nostdlib", # no standard lib
        "-fno-ident",  "-fno-asynchronous-unwind-tables", # Remove unnecessary sections
        "-lkernel32" # Add necessary imports
    print("[+] Compiling stub : "+" ".join(cmd))
    subprocess.run(["strip.exe", output_exe_file])

Rename .packed

We also have a pretty explicit names for our .packed section, maybe we can define it as read-only, and find something more … classic for a read only section : .rodata for example.

packed_section = lief.PE.Section(".rodata")
    packed_section.content =  packed_data
    packed_section.size = len(packed_data)
    packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
                                    | lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)

Now, we use the name .packed to find the section in the unpacker, but we don’t need it : it’s always the last section in the bianry, so let’s simply correct it :

char* packed_PE = unpacker_VA + sections[p_NT_HDR->FileHeader.NumberOfSections - 1].VirtualAddress;

With this modification also goes the .rdata section generated by Mingw32 for the unpacker, as this was the only string we used. We now have only 3 sections for an ASLR enabled packed binary: .text, .idata (the import table of the unpacker) and .rodata (the packed PE).

Simple obfuscation

Now, let’s do some actually useful packing, a do some basic hiding for our packed PE file. Right now it is very obvious to retrieve it from the .rodata section :

PE header in the rodata section

The MZ and DOS stub are really easy to spot. We are going to hide them a bit.

As a simple example, we will simply XOR the input file content. Nothing fancy, but this will demonstrate how easy it is now that we have everything else settled.

Change the packer

Here is a very simple XOR function in python, with a hardcoded key. To make it a bit more complex we use each resulting byte as the key for the next one (CBC cryptographic mode).

def pack_data(data) :
    KEY = 0xAA
    result = [0] * len(data)
    for i in range(0, len(data)):
        KEY = data[i] ^ KEY
        result[i] = KEY
    return result

And call it on the input file data before putting it in the section:

packed_data = pack_data(list(input_PE_data)) # pack the input file data
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(".rodata")
packed_section.content =  packed_data

Change the unpacker

We will hardcode the key in the unpacker as well:

void unpack_data(char* src, DWORD size) {
    DWORD oldProtect;
    //make sure we can write on the destination
    VirtualProtect(src, size, PAGE_READWRITE, &oldProtect);

    DWORD KEY = 0xAA;
    DWORD new_key = 0;
    for(DWORD i=0; i<size; ++i) {
        new_key = src[i];
        src[i] = src[i] ^ KEY;
        KEY = new_key;

And call this function before loading the PE data of course:

unpack_data(packed_PE, sections[p_NT_HDR->FileHeader.NumberOfSections - 1].SizeOfRawData);

Final result

Our packer still works the same, but take a look at the sections:

obfuscated sections

We got less sections, and no obvious one containing the packed file. It’s going to take a (very small) bit of work to extract the packed file for a malware analyst. And an antivirus would not find any signatures simply looking at this file, as everything has been xored (of course it would still be detected dynamically when launched, but still).

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

Go further

There is a lot more that we could do to this packer:

  • Its import table contains the 5 well known functions to import others, which is clearly a malware behavior. We could use lief to add many others that would look less suspicious, even if we are not using them.
  • The XOR is very easy to crack, knowing the first 2 bytes are MZ, the key is trivial to obtain, even without reading the code. We could encrypt the packed binary using a proper algorithm, like AES.
  • We could avoid adding a section altogether, and find the packed binary data some other way (after the last section data, or with a flag to look for).

Leave a Reply

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