Pe File Reader
[Opening .exe files to analyze the PE-header]Hello_Friend,
and welcome to the 20's, Cybermonkeys!
Let's start this decade by learning some stuff.
Last year, we already took an in-depth look at Linux-binaries (aka ELF files), now let's start messing around with it's infamous Windows-Counterpart (that also happens to be the xBox file format).
Introduction
The PE-file format is around for quite some time now, and while heavy optimizations took place, it hasn't really changed all that much since then. In fact, it is one of the most widely encountered file formats out in the wilds. Although there is technically a difference between PE32 files (32bit) and PE32+ files (64bit), we will ignore this fact for the sake of this blogpost.Some file extensions deriving from this format are:
- .acm [ a Windows audio-codec]
- .ax [MPEG-4 DVD format]
- .cpl [dynamic link libraries for control panels]
- .dll [dynamic link libraries]
- .drv [hardware drivers]
- .efi [extensible firmware interface]
- .exe [executable file]
- .mui [multilingual user interface]
- .ocx [activeX form extension]
- .scr [screensaver file format]
- .sys [system file]
- .tsp [truespeech audio format]
There are other types as we will encounter, but this should give you an idea of how common this format(and it's augmentations) is.
Tools and resources
Before we even start thinking about messing around with PE-files, it would help a lot to have some documentation and software to make our lives easier. Luckily for us, Microsoft has released a structured documentation about the PE-file format.Next, it would be nice to have a disassembler to look into the actual file (or at least a hex-viewer). One might ask why we bother to write our own program at all when there are already hundreds and hundreds of good disassemblers out there. The answer is: we are not going to write a disassembler here. We are writing a program to inspect the header file (fast) with the option to augment it so it can modify the behaviour of the executable and bend it to our will (not in this tutorial tho). Personally, I found REDasm to be a nice and fast little program.
Although it can show you program flux and branches and whatnot, I only used it to view the file in hexadecimal format. Here is a picture of the PE-file header I inspected for testing purposes:
As you can see, I am not using much of the fancy functions that the disassembler has, although it is a good tool to start learning reverse-engineering, so kudos to the makers of this tool.
[dark theme kind of sucks, but you have to decide for yourself]
Also it's good to have some hello-world programs ready for testing purposes. Import and export stuff in these programs, respectively, and you will see differences in the binary structure of the files. Besides that we don't really need anything else, I used Visual Studio, but this is dependant on your personal preferences.
Differences and similarities between PE and ELF
For readers of my previous article, this could be quite interesting.In fact, PE-files and ELF-files aren't that much different from each other. But Microsoft has, again, chosen their own weird methods of implementing stuff as we will see. I took a shot from Wikipedia describing the basic structure of the PE-file, you can see it in the picture.
Now, the probably biggest difference is the MS-DOS stub at the beginning of the file.
Yes, you read correctly: MS-DOS!
It's still a thing, even in 2020. Microsoft has chosen this due to downward compatibility reasons. While I can not understand this choice at all, MS probably thought it would be good to have it. What it does is telling that "This program cannot be run in DOS mode" - no shit Sherlock! It is technically possible to specify another stub [with the linker option /stub], but why bother anyways? If anybody fires up an old 16-bit machine and tries to play Call of Duty on it, they must be crazy anyways.
There is a historical importance to this stub, and yes, you could re-write your complete program to run in this 16bit stub, but if you do I will personally come to your home and spank your ass until you apologize.
Another interesting construct is the so called Rich-header right after the MS-DOS stub.
It is essentially Microsoft spying on you for the purpose of "defending against Malware authors".
If you are interested in this topic or simply want to know how to get rid of the Rich-header,
read this article from bytepointer [good page, pretty old tho, recommend this one].
It will show you how to patch your Linker to cut out the Rich-header.
So, the MS-DOS stub starts at 0x40, and including the Rich-header, it usually stretches up to 0x100 or 0x120, somewhere around there. So while we did not find anything in common with ELF-files, we already figured out that each PE-file has an unnecessary overhead of around 100 bytes.
Good job, Microsoft!
Okay, now let's see whether we can find anything that these two file formats DO have in common.
At the "beginning" of each PE-file we can find the PE-file header. If you remember the article about ELF-files, this is something both formats share. It holds basic information about the number of sections, some pointers, stuff like that.
It is essentially the MS counterpart of the ELF header, except it is incomplete.
For whatever reasons, there is another "optional header" following the PE-file header. Now don't get confused here, beside the name there is nothing "optional" about this header. Only these two in conjunction form a complete PE-header. [ffs Microsoft, get your shit together]
Following up we can find the data directories, essentially pointers to the sections at runtime. These are used mostly by the loader, to get to the section offsets really fast when loading up the binary for execution. They don't serve any other purpose.
After that we get right into the sections, or rather, the what-would-be section header table (although MS doesn't call it like that). Each section contained in the program is listed here, with a pointer to the in-memory location of the section. We will get there eventually, but let's talk about sections a bit longer.
For most parts, each section in a PE-file has an almost identical counterpart inside ELF-files, often even sharing the same name. Unlike ELF-files, the PE format does not really distinguish between sections and segments. There are simply sections, period. There is no such thing like a program header table, which may be a curse or a bless, depending on your look at it. The section header table is used for linking and loading the binary, so the PE-files are somewhat more consistent at execution time.
For some reason, compilers like the one in Visual Studio sometimes place read-only data inside the .text section instead of .rdata, mixing it up with executable code. Be cautious, for this can lead to problems during disassembly.
One of the most important sections, .edata and .idata, have no equivalent in ELF-files. They contain exported and imported functions. The .idata section specifies which symbols to import from .DLLs, while the .edata section lists symbols and addresses that the binary exports. In practise, .idata and .edata are often merged withing the .rdata section, but besides that work exactly like I just described.
Resolving of external dependencies works similar to ELF-files, Microsoft just uses a struct called "Import Address Table" (or IAT) instead of the ELFish "Global Offset Table". Microsoft uses so called Thunks for external library calls, which happen to be jump gates of pointers, so basically there is no big difference between Windows-thunks and ELf-stubs.
Another thing to know is that Visual Studio emits int3 instructions to pad and align functions. This has no deeper meaning, you could as well use nop-operations, it's just MS's style to do the same things and call it by different name. The int3 instructions normally serve as breakpoints for debuggers, but since they are placed in the void between actual code sections, this is not really of an issue.
Implementing a File Reader [1] Opening and Reading a binary
Now, let's start implementing our own PE dumper, similar to the one we wrote for ELF files,
but much simpler. Think of it more like a prototype for future projects. I will talk about this at the end of the blogpost. To get to work, we first need to consider some things:
- How will the binary be loaded
- How are we going to orient our self over the file
- How do we actually read values and output them
- How do we confirm that the values are correct
For each of these points, there are slippery slopes and shortcuts, and I had to reorganize the program several times due to false assumptions, so on the way through the code, let me explain how to overcome coding problems with skill rather than with speed.
Let's just look at the beginning of the main function first:
int _tmain(int argc, TCHAR *argv[])
{
//file handles
HANDLE targetBinary;
LPDWORD readBytes = 0;
DWORD fileSize;
//the pointer that will serve as bookmark
int readPointer = 0x0;
//get system info to validate some byte values
SYSTEM_INFO si;
GetSystemInfo(&si);
fprintf(stdout, "The page size for this system is %u bytes.\n", si.dwPageSize);
//handle to file needs to be called via CreateHandle
targetBinary = CreateFile(L"C:/Program Files (x86)/Wizards of the Coast/MTGA/MTGA.exe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
fileSize = GetFileSize(targetBinary, NULL);
fprintf(stdout, "File is %d Kbytes large\n", fileSize/1024);
char *fileContent = new char[fileSize];
if (targetBinary != INVALID_HANDLE_VALUE)
{
DWORD retVal = 0;
//if function successfull, retVal is != 0
if(!(retVal = ReadFile(targetBinary, (LPVOID)fileContent,fileSize, readBytes, NULL)))
{
fprintf(stdout, "There was an error reading input file!: %d returned.\n",retVal);
exit (-1);
}
//fprintf(stdout, "Return value of ReadFile(5) is %d\n", retVal);
}
[ ... ]
[ 1 ] The _tmain function
The first line already looks weird, I admit ^^
If you invoke int _tmain, you enable 16bit modes in C++ code. Well, that's the short version for this, at least.
_tmain is like a switch that automatically decides whether main or wmain should be called, a Microsoft extension.
It enables Unicode (UTF-16) character sets and also swaps the bytes because of it's endianess, thus resulting in the program seeing a bunch of 0 terminated strings rather than char arrays. If you want to know more, go to this Stackoverflow post. We basically Increase the support for modern operation systems and their weird file names while also using it as a kind of safety measure for reading hex values. I'll talk about it in future posts, especially on Assembler.
[ 3-6 ] Handle to load the binary
Remember the first point on our list? Let's start with that. I'm sure many of you will know how to read a file in C++, but let me introduce file Handles to you. A handle is basically a pointer to something. In this case, it's to a file. This pointer helps us to read, write, copy or overwrite said file however we want. It is also useful if we want to modify the binary later. Let's not stick to this for too long, it's a higher level-pointer that's well documented. The LPDWORD and DWORD are both integers that we'll be using to read the binary into memory.
[ 9 ] The binary pointer
One part of the answer to How are we going to orient our self over the file lies within this variable. As the name tries to imply, it will save our current location while moving through the file, but currently it's set to 0 so it just points to the very beginning.
[ 18-19 ] Open FileHandle
At 18-19 we are creating the actual HANDLE file object. The first argument CreateFile takes is a path to a file. We are providing an Unicode version [with the "L" prefix] of the string, so every language should be supported. Also, CreateFile is a nice function, it accepts both forward- and backward-slashes. Way to go, CreateFile!
The next parameters are access rights and some attributes, and all I had to do is look into the documentation and fiddle it out until it worked. Let's skip this trivial path
It's really up to you which binary you pick for testing, but it really is useful to test multiple files during development. You don't want to fall to false assumptions. I was halfway through development when I realized that not all values are at fixed locations. In fact, none of them really are. Alas, had I only tested multiple files first, I had saved at least 2 hours of rewriting and testing stuff.
Fool your assumptinons before getting fooled by your assumptions.
[ 26-37 ] Read the file into memory
We created a file, now it is time to read it. The ReadFile function isn't an easy function to work with. At first I should say that there are 2 versions of the function, a synchronous and an asynchronous one. The latter is required for reading streams, drivers and the like, stuff that changes a lot, and it is called ReadFileEx. We will need a callback here, as you'll notice.
Implementing a File Reader [2] Functions and callback
It's time to look at some functions that we'll be using to step over the file and output stuff. We are going to plan ahead here. Since we want to extend the program in the future, we will already implement a function to output a chunk of hex bytes with line numbering, just like a disassembler. Also, make sure you are really understanding what's going on, especially with the OR-operation. We'll get to this, let's first again look at the code and think about it after wards.
#include <windows.h>
#include <tchar.h>
#include <iostream>
#define QWORD_L 8
#define DWORD_L 4
#define BYTE_L 1
#define WORD_L 2
using namespace std;
//for use in callback
DWORD transfered = 0;
//actual callback for asynchronous reading
VOID CALLBACK finished
(
__in DWORD dwErrorCode,
__in DWORD dwNumberOfBytesTransfered,
__in LPOVERLAPPED lpOverlapped
)
{
_tprintf(TEXT("Error code:\t%x\n"), dwErrorCode);
_tprintf(TEXT("Number of bytes:\t%x\n"), dwNumberOfBytesTransfered);
transfered = dwNumberOfBytesTransfered;
}
//later for oop (parameter called by reference)
void copy_bytes(char* &dump, char* &memory, int start, int end)
{
int counter = 0;
while (start < end)
{
memory[counter++] = dump[start++];
}
return;
}
//give single data fields or output values
//TO DO: write function so you can input start, dword (or whatever)
void output_data(char* memory, int start, int end)
{
if (end < start)
return;
fprintf(stdout, "0x");
while (end >= start)
{
fprintf(stdout, "%02hhX", memory[end]);
end--;
}
}
//same function, but grabbing text this time, so reversed output direction (big-endian)
void output_data_ascii(char* memory, int start, int end)
{
if (end < start)
return;
while (start < end)
{
fprintf(stdout, "%c", memory[start]);
start++;
}
}
//because file shambles
int getValuePNTR(const char* memory, int &start, int size)
{
uint64_tretVal = 0;
//now just add up array fields
for (int i = start + size-1,j = size-1; j >= 0; --j, i--)
{
//fprintf(stdout, "\ncycle: %d, memory: [%x]", j, memory[i]);
if ((unsigned char)memory[i] == 00 && j > 0)
retVal <<= 8;
else
retVal |= ((unsigned char)(memory[i]) << (8 * j));
}
//get the next field after this one
start += size;
return retVal;
}
//overwrite function
int saveValuePNTR(char* memory, int start, int size)
{
uint64_tretVal = 0;
for (int i = start + size - 1, j = size - 1; j >= 0; --j, i--)
{
if ((unsigned char)memory[i] == 00 && j > 0)
retVal <<= 8;
else
retVal |= ((unsigned char)(memory[i]) << (8 * j));
}
return retVal;
}
//output stuff
void output_bytes(char* memory, int start, int end)
{
//tracker for formatting the dump
int counter = 1;
int linebreaker = 0;
int offset = start ? start % 0x10 : 0;
//output the binary, use upper case X in format string for BIG LETTERS
fprintf(stdout, "%010x: ", start - offset);
for (int i = start - offset; i < end; i++)
{
if (counter > 3)
{
// print dots if we have an uneven countVal
if (offset > 0)
{
offset--;
fprintf(stdout, ".. ");
counter = 0;
}
else
{
fprintf(stdout, "%02hhX ", memory[i]);
counter = 0;
}
linebreaker++;
}
else
{
// print dots if we have an uneven countVal
if (offset > 0)
{
offset--;
fprintf(stdout, "..");
}
else //print the opcode
fprintf(stdout, "%02hhX", memory[i]);
}
counter++;
if (linebreaker > 3)
{
fprintf(stdout, "\n");
fprintf(stdout, "%#010x: ", i + 1);
linebreaker = 0;
}
}
cout << endl << endl;
}
It looks longer than it is, really. The hardest part is probably the last one. Right after the imports, I created some macros to represent byte lengths. Now, I know I could just type in the numbers, but it really is better to read, and also did I have problems with the standard ones so I made my own.
[ 1-8 ] The callback for ReadFile
This callback works in conjunction with the ReadFile function we already looked at in the beginning of _tmain. I was just copying it from the Microsoft documentations, so you'd be better off by just searching there. If you just want to understand it for now, it kind of copys chunks of data of fixed size, checking for EOF in between reads. The callback can be used to transmit errors and signals. It bounces a parameter referenced to as "NumbersOfBytesTransfered" over each cycle. There's really not that much up with it. Also it doesn't stop on a NULL-byte because it thinks that it's a string termination, which is kinda nice.
[ 27-37 ] Copy between files
This function can be used to copy chunks of bytes between different files. Remember that we created a file with our file handle, we might want to automatically insert code chunks at special points in the code some time in the future. Consider this function a typical it will come in handy LATER.
[ 41-63 ] Output raw data
These two functions can be used to output data from the binary. You might notice the format string %02hhx. It is a 2 digit hexadecimal format, and you should get used to this kind of format strings.
- 0
- fill the number with zeros to align the digits (f.e. write 0001 instead of 1)
- 2
- the length of the number
- hh
- if followed by a x, this will make the input an unsigned char
- x
- output as hexadecimal number as specified
The third part is crucial. When working with hexadecimal output we are always required to work with unsigned chars, as I had to learn the hard way. If you don't state that you are expecting the values to be unsigned, your output will be totally random, since the first bit in each variable is usually interpreted as the sign. If you get strange readings during testing, check for the signing of especially chars.
You'll notice that for the second function, I broke my own premise of always using unsigned chars. Yikes! The reason for this is simple: we aren't using any hex values here. When dealing with binaries, you'll often encounter data saved in plain text form. Usually strings, but sometimes information for the loader, too. These strings are stored in big-endian, which means they are "forward" written in the file (like in, how a human would read them).
That's great, they made this so we have an easy life reading the strings, right?! NAHAA, it's only because they get read in reverse order into memory later anyway. Don't assume anybody would help you fiddling around with their stuff! [JK lol]
[ 66-98 ] Oh, to read a DWORD
These next functions are really our bread and butter. We are going to call them very often, indeed. And it is them that feature the OR-operation that took me almost 2 weeks. It failed constantly, and I had no idea why.
Both functions start identically: They take a chunk of data as first argument, a pointer into that chunk as second one, and a to-read size as third parameter. One difference you might notice is that the first functions takes the start parameter as a call-by-reference so we can increment it while we are at it. This way you can read values and at the same time increment the file pointer, which saves us some memory.
Both functions first define a uint64_t. Normally I had used DWORD or QWORD here, but recently I found out my program would crash when reading 64bit code, so we are using the 8 byte long variable here all along.
I was going to write how awesome DWORD is because it's managed by operation system instead of language, but uint64_t should be 8 byte long always, too, so we won't get any problems in the future, except maybe someone invents 128bit processors, but this is actually pretty unlikely (in 2020, lul).
Following along, we check whether we read a zero-byte, and if so, we just shift the return variable one byte to the left. This is necessary because we might read a zero as last input, and instead of just adding 00 as the count for position 0, we would shift the first byte out of scope. You can try this out and see it yourself if you want, or you can just trust me on this one.
After we found the byte, we have to shift it to the left a number of times equal to the relative position inside the byte-mask. For example, if we have the value 1 at the third position of the value, we have to shift it to the left three times. This is a bit of a brainfuck at first, especially since values are usually stored little-endian style, but you'll get used to it pretty fast. After the value was shifted, we (logical) OR it against the return variable. This way we insert the correct value byte by byte.
The biggest difference between these two functions is probably that the first one increments the filepointer after reading the value, so we can just read the next, and the next and so forth. The latter doesn't do so, since we want to save the value from the binary AND call the first function after wards to output it, so this is just ease-of-use here.
[ 99 ] Future use as decompiler
This whole function is designed to nicely print out a (big) chunk of data in a form that you would expect from a typical hex-editor. In the whole example program I'm currently not using it for anything, but I tested and fine-tuned it, so feel free to give it a try. It will print out line numbers as well as a little gap between DWORDS, so you can examine the binary. Although I am not really using it (or explaining), it is extremely useful to have for future examinations of binaries, so just keep it there, and maybe play around with it a little.
Implementing a File Reader [3] The main work
After the setup of all our functions and the loading of the binary into memory, it is time to actually get some data out of it. But first we have to solve a riddle in the file:
Where the fuck does it start anyways?
The answer to this question is: it depends...
"Ah, great, another of these undecided binaries", you might think.
"Get your shit together, man!"
In fact the first value we need to extract from the binary is the most crucial for us at this point: The value at 0x3c. This magic number is an address at which we can find the PE-signature, and after that the real information is hidden. In it's original form my program featured a function that just hard-reads this value, but since we have all our nice functions at the top, we are going to use them right away. Let's take a look:
//first, get that 0x3c word so we know where to start reading
readPointer = saveValuePNTR(fileContent, 0x3c, WORD_L);
fprintf(stdout, "\nINITIALISING....\nReadpointer before operation: %x",readPointer);
fprintf(stdout, "\n[DWORD] PE-SIGNATURE :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nReadpointer after operation: %x\nInitialising finished", readPointer);
//first block: PE FILE HEADER
fprintf(stdout, "\n\nCollect Information (PE file header):");
fprintf(stdout, "\n[WORD] Mashinae Type :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
unsigned int sectionCount = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Number of Sections :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[DWORD] Timestamp :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Pointer to symbol table:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Number of Symbols :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[WORD] Size of optional header:0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] Characteristics :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
Right at the start, we first grab the information about the PE-Signature, as I said it is located at 0x3c, and it is 2 byte long, hence the WORD_L. After that, I wrote a little initialization output just to be sure everything works as expected. Notable are the use of the fprintf function. By using this, we can later swap out the stdout with a file pointer, so we can easily create log files about our readings.
I decided to use the format string 0x%08x to give the output a fixed length of 8 digits. This will have to be tweaked for QWORDS later on.
The first block we are going to read is the PE file header. You can see in line 10 how we are able to save some information that will come in handy later with the help of the saveValuePNTR function. The rest is just outputting information for now. This is pretty much how the rest of the code looks, with some exeptions for 64bit systems, as you can see below:
//second block: OPTIONAL HEADER
fprintf(stdout, "\n\nOPTIONAL HEADER [general fields]:");
unsigned int type = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Architecture :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
if (type == 0x010b)
fprintf(stdout, " -> [32 bit]");
else if (type == 0x20b)
fprintf(stdout, " -> [64 bit]");
else if (type == 0x107)
fprintf(stdout, " -> [ROM img]");
else
fprintf(stdout, " -> [unknown]");
fprintf(stdout, "\n[BYTE] Major Linker Version :0x%08x", getValuePNTR(fileContent, readPointer, BYTE_L));
fprintf(stdout, "\n[BYTE] Minor Linker Version :0x%08x", getValuePNTR(fileContent, readPointer, BYTE_L));
fprintf(stdout, "\n[DWORD]Size of Code :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]SizeofInitialisedData :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]SizeifUninialisedData :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]Adress of Entrypoint :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]CodeBase :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
//next field depending on arch (only present in 32bit executables)
if (type == 0x010b)
fprintf(stdout, "\n[DWORD]Base of Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n\nOPTIONAL HEADER [windows only fields]:");
fprintf(stdout, "\n[DWORD / QWORD]ImageBase :");
unsigned int base;
//next field depending on arch, 4 byte in 32bit, 8 byte in 64bit
if (type == 0x010b)
{
base = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
//fprintf(stdout, "\nDEBUG: 0x%08x , pointer @ %x \n", base,readPointer);
}
else if (type == 0x020b)
{
base = saveValuePNTR(fileContent, readPointer, QWORD_L);
fprintf(stdout, "0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
}
else
base = 0xFFFFFFFF;
unsigned int offset_segment = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "\n[DWORD]Section Alignement :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n...beginning of text segment @ 0x%08x", (base + offset_segment));
fprintf(stdout, "\n[DWORD]File Alignement :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[WORD] OS VERSION major :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] OS VERSION minor :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] image version major :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] image version minor :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] subsystem major version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] subsystem minor version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[DWORD] xxxXXX ZERO VAL XXXxxx :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Imagesize in bytes :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Size of headers :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Checksum :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
unsigned int subsystem = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Subsystem version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
unsigned int dll_characteristic = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] DLL characteristics :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
//the next 4 values depend on architecture
if (type == 0x010b)
{
fprintf(stdout, "\n[DWORD] SizeOfStackReserve :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfStackCommit :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfHeapReserve :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfHeapCommit :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
}
else if (type == 0x20b)
{
fprintf(stdout, "\n[QWORD] SizeOfStackReserve :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfStackCommit :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfHeapReserve :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfHeapCommit :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
}
We are going to read the rest of the header and optional header. At some points, like in lines 5-12, it is necessary to differentiate between x86 and x86-64 systems. We can make this differentiation with the help of the value we read in line 3. This is one of the more crucial information we have to gather, as it is used further below again. Also, note that we have to expand the format string to 0x%016x if we are to output 64bit information.
Since a single byte needs 2 digits to be displayed ( from range 00 to ff ), we have to expand the format string in these cases. Luckily, there are only minor differences between 32bit and 64bit binaries. I suggest you read into the Microsoft Documentation, as I stated earlier.
In the documentation, you can see the length of the fields in each architecture, so we just have to insert some switches there and use our existing functions to expand to a QWORD_L there. Some values are of mere WORD length, and some are even single BYTE.
Coming up next are the Data Directories. These are essentially pointers into sections, and they are primarily for the linker, so that he has some shortcuts when setting up the process into memory. You CAN, however, use them to find important sections like the Import Table (which is one of the most important ones), so when augmenting a program, these pointers will be useful to your augmentor program.
unsigned int remainder = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "\n[DWORD] Remaining header count :0x%08x [Remaining data directories]", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer now @ %x", readPointer);
//next up are the data directories, there are usually 16 of them
//each is 8 byte, so 16 * 8 = 128 >> size of directory entries
if (remainder == 16)
{
fprintf(stdout, "\n\n############################################################\nData Directory entries:");
fprintf(stdout, "\nExport Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nImport Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nResource Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nException Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nCertification Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nBase Relocation Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nDebug :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nArchitecture :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nGlobal PTR :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nTLS Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nLoad Config Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nBound Import :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nIAT :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nDelay Import Descriptor:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nCLR Runtime Header :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nReserved Zero Value :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nEnd Data Directories, PNTr now @ %x", readPointer);
}
else
{
fprintf(stdout, "\n\nDetected non-standart data directories, recalculating Pointer ...");
readPointer += (remainder) * 8;
fprintf(stdout, "\nEnd Data Directories, PNTr now @ %x", readPointer);
}
At first, we save the remainder of data directories. Now, don't get confused here, a PE file usually has 16 data directories. ALWAYS. But when, for example, the remainder counts 10, it means that some directories are empty. For example, a binary may not export anything, or does not have a Debug section. If, for whatever reason, the binary has more or less than 16 data directories, we skip the output (for now), and recalculate the beginning of the section headers.
The else part is wrong at the moment!
As I just described, the remainder is not the actual number of data directories, but I will just leave it as it is for now, it should not be too hard to recalculate if you ever have the need for it.
There is only one part left: The actual section headers, so let's dive right in:
/*
Following section headers
Each section header is 40 byte large (5 DWORDS)
You can calculate the beginning of the headers,
but we came here anyways so let's skip that part
*/
fprintf(stdout, "\n\n########## BEGINNING OF SECTION HEADERS ##########\n");
for (int i = 0; i < sectionCount; i++)
{
//first output the name in plain ascii, big-endian
fprintf(stdout, "\n\n################### ");
output_data_ascii(fileContent, readPointer, readPointer + 8);
fprintf(stdout, " ###################");
readPointer += 8;
fprintf(stdout, "\nVirtual Size :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nVirtual Address :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nSize of Raw Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to Raw Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to Relocations :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to line numbers:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nNumber of Relocations :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\nNumber of line numbers :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\nCharacteristix :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
}
//close handle, end of operations
CloseHandle(targetBinary);
std::cout << "\nEnd reached!\n";
return 0;
}
This is the last part of our program so far, and it will grab the section headers from the binary.
Each section header has a fixed size and the same values associated with it, so we can just use a loop to grab all of them. Since the name of each section is stored in big-endian, aka human-readable way, we have to call our output_data_ascii function here. There isn't really any magic in here, nothing you didn't already encounter. Note that we manually have to increment the readPointer variable in each loop, since the output_data_ascii function doesn't do this for us. The amount of section headers is already stored in the variable sectionCount, so we always have the right amounts of loops ready.
At the end we need to close the handle to prevent memory leakage and overflowing.
Full code
Here I want to paste the full code in a single file so it's easier to just copypasta it if you want to try it out yourself:
#include <windows.h="">
#include <tchar.h="">
#include <iostream>
#define QWORD_L 8
#define DWORD_L 4
#define BYTE_L 1
#define WORD_L 2
using namespace std;
//for use in callback
DWORD transfered = 0;
//actual callback for asynchronous reading
VOID CALLBACK finished
(
__in DWORD dwErrorCode,
__in DWORD dwNumberOfBytesTransfered,
__in LPOVERLAPPED lpOverlapped
)
{
_tprintf(TEXT("Error code:\t%x\n"), dwErrorCode);
_tprintf(TEXT("Number of bytes:\t%x\n"), dwNumberOfBytesTransfered);
transfered = dwNumberOfBytesTransfered;
}
//later for oop (parameter called by reference)
void copy_bytes(char* &dump, char* &memory, int start, int end)
{
int counter = 0;
while (start < end)
{
memory[counter++] = dump[start++];
}
return;
}
//give single data fields or output values
//TO DO: write function so you can input start, dword (or whatever)
void output_data(char* memory, int start, int end)
{
if (end < start)
return;
fprintf(stdout, "0x");
while (end >= start)
{
fprintf(stdout, "%02hhX", memory[end]);
end--;
}
}
//same function, but grabbing text this time, so reversed output direction (big-endian)
void output_data_ascii(char* memory, int start, int end)
{
if (end < start)
return;
while (start < end)
{
fprintf(stdout, "%c", memory[start]);
start++;
}
}
//because file shambles
int getValuePNTR(const char* memory, int &start, int size)
{
uint64_t retVal = 0;
//now just add up array fields
for (int i = start + size-1,j = size-1; j >= 0; --j, i--)
{
//fprintf(stdout, "\ncycle: %d, memory: [%x]", j, memory[i]);
if ((unsigned char)memory[i] == 00 && j > 0)
retVal <<= 8;
else
retVal |= ((unsigned char)(memory[i]) << (8 * j));
}
//get the next field after this one
start += size;
return retVal;
}
//overwrite function
int saveValuePNTR(char* memory, int start, int size)
{
uint64_t retVal = 0;
for (int i = start + size - 1, j = size - 1; j >= 0; --j, i--)
{
if ((unsigned char)memory[i] == 00 && j > 0)
retVal <<= 8;
else
retVal |= ((unsigned char)(memory[i]) << (8 * j));
}
return retVal;
}
//output stuff
void output_bytes(char* memory, int start, int end)
{
//tracker for formatting the dump
int counter = 1;
int linebreaker = 0;
int offset = start ? start % 0x10 : 0;
//output the binary, use upper case X in format string for BIG LETTERS
fprintf(stdout, "%010x: ", start - offset);
for (int i = start - offset; i < end; i++)
{
if (counter > 3)
{
// print dots if we have an uneven countVal
if (offset > 0)
{
offset--;
fprintf(stdout, ".. ");
counter = 0;
}
else
{
fprintf(stdout, "%02hhX ", memory[i]);
counter = 0;
}
linebreaker++;
}
else
{
// print dots if we have an uneven countVal
if (offset > 0)
{
offset--;
fprintf(stdout, "..");
}
else //print the opcode
fprintf(stdout, "%02hhX", memory[i]);
}
counter++;
if (linebreaker > 3)
{
fprintf(stdout, "\n");
fprintf(stdout, "%#010x: ", i + 1);
linebreaker = 0;
}
}
cout << endl << endl;
}
int _tmain(int argc, TCHAR *argv[])
{
//file handles
HANDLE targetBinary;
LPDWORD readBytes = 0;
DWORD fileSize;
//the pointer that will serve as bookmark
int readPointer = 0x0;
//get system info to validate some byte values
SYSTEM_INFO si;
GetSystemInfo(&si);
fprintf(stdout, "The page size for this system is %u bytes.\n", si.dwPageSize);
//handle to file needs to be called via CreateHandle
targetBinary = CreateFile(L"C:/Program Files (x86)/Wizards of the Coast/MTGA/MTGA.exe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
fileSize = GetFileSize(targetBinary, NULL);
fprintf(stdout, "File is %d Kbytes large\n", fileSize/1024);
char *fileContent = new char[fileSize];
if (targetBinary != INVALID_HANDLE_VALUE)
{
DWORD retVal = 0;
//if function successfull, retVal is != 0
if(!(retVal = ReadFile(targetBinary, (LPVOID)fileContent,fileSize, readBytes, NULL)))
{
fprintf(stdout, "There was an error reading input file!: %d returned.\n",retVal);
exit (-1);
}
//fprintf(stdout, "Return value of ReadFile(5) is %d\n", retVal);
}
//first, get that 0x3c word so we know where to start reading
readPointer = saveValuePNTR(fileContent, 0x3c, WORD_L);
fprintf(stdout, "\nINITIALISING....\nReadpointer before operation: %x",readPointer);
fprintf(stdout, "\n[DWORD] PE-SIGNATURE :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nReadpointer after operation: %x\nInitialising finished", readPointer);
//first block: PE FILE HEADER
fprintf(stdout, "\n\nCollect Information (PE file header):");
fprintf(stdout, "\n[WORD] Mashinae Type :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
unsigned int sectionCount = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Number of Sections :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[DWORD] Timestamp :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Pointer to symbol table:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Number of Symbols :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[WORD] Size of optional header:0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] Characteristics :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
//second block: OPTIONAL HEADER
fprintf(stdout, "\n\nOPTIONAL HEADER [general fields]:");
unsigned int type = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Architecture :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
if (type == 0x010b)
fprintf(stdout, " -> [32 bit]");
else if (type == 0x20b)
fprintf(stdout, " -> [64 bit]");
else if (type == 0x107)
fprintf(stdout, " -> [ROM img]");
else
fprintf(stdout, " -> [unknown]");
fprintf(stdout, "\n[BYTE] Major Linker Version :0x%08x", getValuePNTR(fileContent, readPointer, BYTE_L));
fprintf(stdout, "\n[BYTE] Minor Linker Version :0x%08x", getValuePNTR(fileContent, readPointer, BYTE_L));
fprintf(stdout, "\n[DWORD]Size of Code :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]SizeofInitialisedData :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]SizeifUninialisedData :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]Adress of Entrypoint :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD]CodeBase :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
//next field depending on arch (only present in 32bit executables)
if (type == 0x010b)
fprintf(stdout, "\n[DWORD]Base of Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n\nOPTIONAL HEADER [windows only fields]:");
fprintf(stdout, "\n[DWORD / QWORD]ImageBase :");
unsigned int base;
//next field depending on arch, 4 byte in 32bit, 8 byte in 64bit
if (type == 0x010b)
{
base = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
//fprintf(stdout, "\nDEBUG: 0x%08x , pointer @ %x \n", base,readPointer);
}
else if (type == 0x020b)
{
base = saveValuePNTR(fileContent, readPointer, QWORD_L);
fprintf(stdout, "0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
}
else
base = 0xFFFFFFFF;
unsigned int offset_segment = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "\n[DWORD]Section Alignement :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n...beginning of text segment @ 0x%08x", (base + offset_segment));
fprintf(stdout, "\n[DWORD]File Alignement :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[WORD] OS VERSION major :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] OS VERSION minor :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] image version major :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] image version minor :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] subsystem major version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[WORD] subsystem minor version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\n[DWORD] xxxXXX ZERO VAL XXXxxx :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Imagesize in bytes :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Size of headers :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] Checksum :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
unsigned int subsystem = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] Subsystem version :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
unsigned int dll_characteristic = saveValuePNTR(fileContent, readPointer, WORD_L);
fprintf(stdout, "\n[WORD] DLL characteristics :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
//the next 4 values depend on architecture
if (type == 0x010b)
{
fprintf(stdout, "\n[DWORD] SizeOfStackReserve :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfStackCommit :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfHeapReserve :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\n[DWORD] SizeOfHeapCommit :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
}
else if (type == 0x20b)
{
fprintf(stdout, "\n[QWORD] SizeOfStackReserve :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfStackCommit :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfHeapReserve :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
fprintf(stdout, "\n[QWORD] SizeOfHeapCommit :0x%016x", getValuePNTR(fileContent, readPointer, QWORD_L));
}
fprintf(stdout, "\n[DWORD] LoaderFlag (zeroVal) :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
unsigned int remainder = saveValuePNTR(fileContent, readPointer, DWORD_L);
fprintf(stdout, "\n[DWORD] Remaining header count :0x%08x [Remaining data directories]", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer now @ %x", readPointer);
//next up are the data directories, there are usually 16 of them
//each is 8 byte, so 16 * 8 = 128 >> size of directory entries
if (remainder == 16)
{
fprintf(stdout, "\n\n############################################################\nData Directory entries:");
fprintf(stdout, "\nExport Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nImport Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nResource Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nException Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nCertification Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nBase Relocation Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nDebug :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nArchitecture :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nGlobal PTR :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nTLS Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nLoad Config Table :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nBound Import :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nIAT :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nDelay Import Descriptor:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nCLR Runtime Header :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nReserved Zero Value :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, " [Size] :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nEnd Data Directories, PNTr now @ %x", readPointer);
}
else
{
fprintf(stdout, "\n\nDetected non-standart data directories, recalculating Pointer ...");
readPointer += (remainder) * 8;
fprintf(stdout, "\nEnd Data Directories, PNTr now @ %x", readPointer);
}
/*
Following section headers
Each section header is 40 byte large (5 DWORDS)
You can calculate the beginning of the headers,
but we came here anyways so let's skip that part
*/
fprintf(stdout, "\n\n########## BEGINNING OF SECTION HEADERS ##########\n");
for (int i = 0; i < sectionCount; i++)
{
//first output the name in plain ascii, big-endian
fprintf(stdout, "\n\n################### ");
output_data_ascii(fileContent, readPointer, readPointer + 8);
fprintf(stdout, " ###################");
readPointer += 8;
fprintf(stdout, "\nVirtual Size :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nVirtual Address :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nSize of Raw Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to Raw Data :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to Relocations :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nPointer to line numbers:0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
fprintf(stdout, "\nNumber of Relocations :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\nNumber of line numbers :0x%08x", getValuePNTR(fileContent, readPointer, WORD_L));
fprintf(stdout, "\nCharacteristix :0x%08x", getValuePNTR(fileContent, readPointer, DWORD_L));
}
//close handle, end of operations
CloseHandle(targetBinary);
std::cout << "\nEnd reached!\n";
return 0;
}
Good thing is that I had the chance to fix last-minute bugs while writing this blogpost. Yay!
Conclusion
I hope I could show you that it is really not that hard to read out a binary if you have a fundamental understanding of how operating systems work. Of course there are some major differences between Windows, Linux, Arm (like Raspbian, which basically is a Linux, but on a different processor) and other platforms, but in general they tend to be very similar to each other. For example, there is always a header, a separate code section and sections that are write-protected. And of course Microsoft needed to do their own thing, and although I prefer ELF files myself, there is not that much difference to PE files all in all. Here is a final picture of the program in action:
Where to go from here?
Well, I hope to write a code-injection tutorial in the future, so you can consider this the basics of this topic. We took a deep dive into the structure of PE files here, but we haven't even looked at actual processor instructions inside the code sections. Don't worry, tho, we'll get there eventually. For now, try to open different files with this code, and try to dump code sections with the help of the currently unused output_bytes function. In fact, try to expand this program to actually manipulate values in the target binary. If you manage to do this, you are a big step further at becoming a professional hacker. If you have questions, feel free to ask them to me, but don't expect me to know everything ^^
Some good sources to this topic, although for Linux, are Dennis Andriesse's Practical Binary Analysis and Ryan O'Neil's Learning Linux Binary Analysis.
In fact, I came up with this whole code myself after reading Mr. Adriesses book, which is a major pro for this book, since it imparts knowledge that helps to come up with own solutions rather than just copypasting from the book. I really like this topic and will definately focus my research more on this, but one step after another, chummer...
That's it, I hope you liked this article! Thank you for reading it to the end.
If you really want to understand something, you have to tear it apart.
So go out there and rip the world into pieces.
So go out there and rip the world into pieces.
- numb.3rs
Really cool article, very inspiring
ReplyDeleteJust one typo, author of "Practical Binary Analysis: Build Your Own Linux Tools for Binary Instrumentation, Analysis, and Disassembly" is not Dennis Adriesses but Dennis Andriesse
Thank you for the feedback! I will correct this asap, unfortunately I don't have that much time for this blog at the moment. As soon as I solved some issues irl, I will continue
Delete