Table of Contents
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(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 :
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:
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).