Building a PE from scratch
2021-03-11
A little while ago I got the idea that it might be fun to try and create a PE from scratch using Python. I guess my idea of having fun is pain, but also it's useful to be able to create a PE from scratch when you want to debug some shellcode, another one of those fun things.
This post will document my thought process and might be just for my own reference, and it might help people that want to have fun like I did as well.
PE Format
While I'll try not to bore you with explaining the full PE format, it's still important to go into the different components that make up the PE format to get an overview of the stuff we need to build it, and to end up with something that a debugger like x64dbg understands.
The PE format can be divided in the following components:
DOS Header
COFF File Header
Optional Header
Section Table
While not all fields need to have a value for the PE to be valid (which TinyPE shows pretty nicely, your browser will mark this as a harmful website btw), I am going to try and be as complete as I can while building this. The fields which can be left with a null value will likely be skipped for the sake of the length of this post, please refer to this Microsoft page that will answer all of your PE questions.
DOS Header
The DOS header part is actually fairly easy since only a couple of things are really important if you want your PE to be valid:
Magic number
Location of the COFF File Header
The magic number is nothing more than MZ
, or 4D5A
when talking hex. There's not much reason for this to be MZ other than that these are the initials of Mark Zbikowski, the godfather of the format. For a small introduction to the MZ header you can refer to this blog. The magic number consists of the first two bytes in the DOS header.
The other important field to include is the location of the file header, also known as the PE header. This value is actually a pointer to the offset where this PE
string can be found in the binary. For the purposes of keeping everything simple and stupid I set the offset to 0x100
I will be using the Python struct library to pack everything while building the final binary. Please note that the endianness is little.
Followed at the end I included the DOS stub, this is a legacy text to mark that this executable will not run in DOS mode (click for more information).
COFF File Header
This header is a little more interesting, but only a little.
We don't need to provide a value for all of these fields as well, the following fields are necessary:
Signature
Machine
NumberOfSections
SizeOfOptionalHeader
Characteristics
Signature
The Signature is a simple field and just denotes PE\0\0
this value needs to be at the offset we provided when we created the DOS header.
Machine
Since we're creating an executable for a 32-bit machine we will use the 0x14c
value to specify this in the Machine field.
NumberOfSections
The NumberOfSections field is used to specify how many sections we are including in this executable. Sections are used for different purposes, the most widely known sections are probably the .text
section which holds the executable code, and the .idata
section which holds the imports that are being used. Since we're only interested in building a debuggable executable with the shellcode of our choice, we will only include one section, the .text
section. If you do want to learn more about sections while keeping a semi-highlevel overview, please refer to this article.
SizeOfOptionalHeader
Next up we provide the size that the optional header has, this is a trick question because as you can see in this nice PE format overview, the optional header comes after the COFF file header... I cheated a bit and provided my Python code with the size since I build each part of the header individually, but if you do want to play it safe you can just put 224 bytes as size and make sure to include every field in your executable.
We'll get to what makes it this size in a little bit.
Characteristics
Then it's time to specify a value for the Characteristics field, a field that determines your executable's faith. There's a lot of different values that you can specify, and even combine. For the sake of this post I will just spoil that the actual value I used is 0x02
, telling the machine this is running on that it's an executable image. Please refer to this table to see a list with possible values and their definitions.
Optional Header
The optional header is without a doubt the most interesting header in my opinion. I had a lot of fun (pain) getting this right, and I probably made a mistake describing certain things (please let me know).
Magic
Now this seems to be a trend at this point, but this header too has a magic value. We specify 0x10b
for 32-bit executables, and 0x20b
for 64-bit executables.
Linker Major/Minor version
To be completely honest I'm at the time of writing unsure what these values entail exactly other than the version of the linker, which seems to not do anything as leaving it 0 is fine as well. Do note that these are single byte values.
SizeOfCode
The size of code is... The size of the code.
AddressOfEntrypoint and BaseOfCode
I was very confused by these fields until I looked at other executables with PE-bear (great tool btw, s/o to @hasherezade). In our case we can leave this the same, but in some cases it could be that the entry point of the code is not at the start of the code section.
BaseOfData
I used a fake value for the BaseOfData field since we don't use a .data
section, if you were to use one, use a value that corresponds with that RVA.
ImageBase
ImageBase is kind of a funny field since it only specifies the preferred starting address of the executable when loaded into memory. This means, it happens only when your machine feels like doing so. Jokes aside though, in my testing it has always loaded the executable at this address in memory.
SectionAlignment and FileAlignment
These two fields are kind of important since they dictate how you should align different parts in your executable, meaning it will need to round upwards of this value if you're no aligned to this value. The values chosen in my scenario are kind of generic;
Section alignment of 4096 bytes
File alignment of 512 bytes
OS Version Major/Minor
These fields just specify the required operating system. Refer to this table for a nice overview of the versions out there.
Major/MinorSubsystemVersion
Indicates the Windows NT Win32 subsystem, this uses the same values as depicted in the table that was linked for the OS major/minor versions.
SizeOfImage
The complete size of the executable when it is loaded into memory, take into account that this value is a multiple of the SectionAlignment
value. To calculate this value we use the VirtualAddress
+ VirtualSize
of the last section in the sections table. Because I'm only going to use 1 section (the .text
section), the only value which will be different depending on the shellcode is the VirtualSize
:
If we would build everything in chronological order we would not know the VirtualSize
value, in the final script the .text
section will be build beforehand so we can use the size of this section to calculate the SizeOfImage
value. The VirtualAddress
field can be set to anything really as this just marks the relative address when loaded into memory, as long as this value is used for the VirtualAddress
field in the .text
section.
SizeOfHeaders
The combined size of an MS-DOS stub, PE header, and section headers rounded up to a multiple of FileAlignment
value.
SizeOfStackReserve, SizeOfHeapReserve and SizeOfHeapCommits
To be completely honest, I copied these values from other executables. I am not sure if an executable can be valid without these set to a value, in my testcases it would end up in a non-debuggable executable.
NumberOfRvaAndSizes
This just indicates the number of data directories that are included in this executable, you can set this to 0 if you don't plan to include any data directories like the ImportDirectory
etc.
In my case I set this to 10
which means I include all 16 data directories, this doesn't mean that I actually will, I only need to take into account that I add enough zeroes after the NumberOfRvaSizes
to make up the size it would normally take for these data directories.
At the time of writing I don't plan to include a way to add imports, I might add this in another post as I'm currently still struggling with wrapping my head exactly as to how this process would end up working when building it from scratch (I'm sure there's people reading this shaking their head because it will turn out to be simple).
Data Directories
I will not go over each of the data directories to explain what they do exactly as they're currently not necessary for the working executable, I will just show you the layout of this.
The way I described it above is actually not the way it is supposed to be included. In a normal executable that will use the ImportDirectory
for example, the structure would look like this;
Each data directory entry consists of the RVA, which is the offset relative to the start of the executable when it is loaded into memory, and the size of the data directory.
Section Table
We're almost done building our executable from scratch, we just need to add one very important section table; the .text
section, this will hold our own shellcode.
The way the sections table is build up is easiest to understand when looking at the C struct which I stole from here;
So in our case we would create our .text
section something like this:
char Name -> this is set to
.text\x00\x00\x00
because we need this to be 8 bytes in size;PyshicalAddress -> this is something that still confuses me, but apparently this is not explicitly specified in the PE(?);
VirtualSize -> this corresponds to the size that will be needed in memory to store the shellcode, be aware that this needs to be aligned to whatever value you set for
FileAlignment
;VirtualAddress -> the relative offset from the start of the executable in memory, I have set this to
0x1000
but you can set this to whatever suits your needs;SizeOfRawData -> the size of your shellcode;
PointerToRawData -> the absolute offset for your shellcode in the binary itself, I have set this to
0x400
and made sure to let my shellcode start at this offset;Characteristics -> the memory attributes to set the initialized memory segment to when loading the shellcode into memory. I have set this to
RWX
which stands for Read, Write, and Execute. For a full list of possible values you can refer to this table;
If you want to add more sections you can do so by repeating the above structure and specifying the right offsets and values needed as Characteristics
Shellcode
Now we just need to add some shellcode and test the thingy right? Yes, yes that's exactly what we're going to do.
For my first testing scenario I've developed the sophisticated shellcode of a function prologue, some instructions, and a function epilogue. Click here if you're not familiar with the epilogue/prologue terms.
After adding this amazing piece of shellcode we can verify with PE-bear that it is present at the offset we provided in the .text
section:
Ok, but can you debug it?
Popping calc.exe
No proof-of-concept is complete without a calc.exe process, to demonstrate our life's achievements to our significant others, we show them a calculator.
To spawn a calc.exe process we simply need to replace our nation-state shellcode with some calculator shellcode, luckily packetstormsecurity got us covered. Replace the old with the new;
If we now run our Python script, step until we reach the instruction after call EAX
which calls the WinExec
function, a calc.exe process is started:
Conclusion
Researching this was quite fun and turned out to be actually useful for whenever I need to load some shellcode into x64dbg without having to attach it after using blobrunner (great tool btw).
I have published my code to my github repository, feel free to shoot me a message if something I described is wrong, or could be described better :)
Last updated