[Exploit development] 5- Dealing with Windows PE files programmatically
Intro
Welcome to our in-depth exploration of Windows PE files. Understanding the PE format is crucial for cybersecurity professionals, as it provides insight into the architecture and functioning of Windows executables. In this article, we delve into parsing PE files programmatically, a skill essential for analyzing and exploiting software vulnerabilities. Although we won’t examine every detail of the PE format, we’ll focus on the most pertinent aspects that are essential for cybersecurity experts. For foundational knowledge, I recommend reading the previous part, which offers a theoretical overview of PE files, their structure, and key concepts related to this topic. Let’s embark on this technical journey to enhance our understanding and skills in handling Windows PE files.
Important Concepts/Notes
Before we get into the article, we have to understand important concepts that we’re going to see a lot.
-
RVA (Relative Virtual Address). an RVA is the address of an item after it is loaded into memory, with the base address of the image file subtracted from it. Because we will parse the PE from the hard drive, most of the time we will have RVAs that need to be converted to file offset. I’ve developed this function for this purpose:
DWORD ResolveOffset(LPVOID lpBaseAddress, DWORD dwRVA) { DWORD_PTR dwPtr; DWORD dwNumberOfSections; // Nt Headers dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew; // Retrieve number of sections from the file header dwNumberOfSections = ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections; // Jump to the section header dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader + ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.SizeOfOptionalHeader; // Iterate over all sections header while ( dwNumberOfSections-- ) { // Check if the given RVA in the range of this section if ( dwRVA >= ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress && dwRVA < ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress + ((PIMAGE_SECTION_HEADER) dwPtr)->SizeOfRawData ) // Calculate the file offset return dwRVA - ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress + ((PIMAGE_SECTION_HEADER) dwPtr)->PointerToRawData; // Next section header dwPtr += sizeof(IMAGE_SECTION_HEADER); } // Invalid RVA return 0; }
Don’t worry, I will explain these complicated things and you will understand, what I want you to know now is how to calculate the file offset from an RVA, the algorithm works as follows:
- Iterate over all sections to determine which section the RVA lives in.
- Calculate file offset using this equation
RVA - SectionVirtualAddress + SectionRawOffset
.
-
DWORD_PTR: A 32/64-bit numerical data type, we will use this type to do pointer calculations easily.
-
Any type starting with P is a pointer. For example,
PIMAGE_DOS_HEADER pDos;
is a pointer equivalent toIMAGE_DOS_HEADER *pDos;
.
Prepare PE parser
Let’s start writing our PE parser. First, we implement the main function that takes the PE file name via command line arguments.
int main(int argc, char **argv)
{
LPVOID lpBuffer;
int nRet = EXIT_FAILURE;
if ( argc <= 1 )
return puts("Usage:\n\t./PEParser </path/to/pefile>");
lpBuffer = ReadPEFile(argv[1]);
if ( !lpBuffer )
{
printf("Failed to read %s\n", argv[1]);
goto LEAVE;
}
/*
PARSE HERE
*/
nRet = EXIT_SUCCESS;
LEAVE:
if ( lpBuffer ) HeapFree(GetProcessHeap(), 0, lpBuffer);
return nRet;
}
The main function reads the PE file by using the ReadPEFile
function, which is implemented as follows:
LPVOID ReadPEFile(char *cpFileName)
{
HANDLE hFile = NULL;
LPVOID lpBuffer = NULL;
DWORD dwSize, dwRead;
// Get a handle on the file with some attributes.
hFile = CreateFileA( cpFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
// If the given PE file doesn't exist, return NULL
if ( hFile == INVALID_HANDLE_VALUE )
goto LEAVE;
dwSize = GetFileSize(hFile, NULL);
// If the given PE file was empty or invalid, close the file handle, return NULL
if ( dwSize == INVALID_FILE_SIZE || dwSize == 0 )
goto LEAVE;
// Allocate sufficient memory for the PE in the main heap
lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwSize);
// If allocation request failed, return NULL
if ( ! lpBuffer )
goto LEAVE;
// Read the PE file in the allocated memory
if ( ! ReadFile(hFile, lpBuffer, dwSize, &dwRead, NULL) )
{
// If we failed to read
// Deallocate the memory, close the file handle, and return NULL
HeapFree(GetProcessHeap(), 0, lpBuffer);
lpBuffer = NULL;
}
LEAVE:
if ( hFile ) CloseHandle(hFile);
return lpBuffer;
}
PE Headers
Windows PE headers contain a lot of valuable information, let’s take a look at them.
DOS Header
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
- e_magic: The image magic number (MZ)
- e_lfanew: An RVA of
IMAGE_NT_HEADERS
, this member very important for the loader which tells the loader where to look for the file header.
NT Headers
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
-
Signature: A signature identifying the file as a PE image (PE\0\0)
-
FileHeader: Contains valuable information about the PE file, this also called The COFF File Header
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
- Machine: Indicates the type of machine (CPU Architecture) the executable is running on. This value can be (IMAGE_FILE_MACHINE_I386 for x86, IMAGE_FILE_MACHINE_IA64 for Intel Itanium, IMAGE_FILE_MACHINE_AMD64 for x64)
- NumberOfSections: The number of sections. This indicates the size of the section table, which immediately follows the headers.
- TimeDateStamp: This represents the date and time the image was created by the linker.
- SizeOfOptionalHeader: The size of the optional header, in bytes.
-
OptionalHeader: Contains valuable information about the PE file like FileHeader with more details.
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
-
Magic: The state of the image file.
-
SizeOfCode: The size of the code section (
.text
), in bytes, or the sum of all such sections if there are multiple code sections. -
SizeOfInitializedData: The size of the initialized data section (
.data
), in bytes, or the sum of all such sections if there are multiple initialized data sections. -
SizeOfUninitializedData: The size of the uninitialized data section, in bytes (
.bss
), or the sum of all such sections if there are multiple uninitialized data sections. -
AddressOfEntryPoint: An RVA of the entry point function, For executable files, this is the starting address. For device drivers, this is the address of the initialization function. The entry point function is optional for DLLs. When no entry point is present, this member is zero.
-
BaseOfCode: An RVA of the beginning of the code section (
.text
). -
ImageBase: The preferred address of the first byte of the image when it is loaded in memory. This value is a multiple of 64K bytes.
-
SizeOfImage: The size of the image, in bytes, including all headers.
-
SizeOfHeaders: The size of the headers including
IMAGE_DOS_HEADER
,IMAGE_FILE_HEADER
, the size of optional header, and the size of all section headers. -
DataDirectory: An array of
IMAGE_DATA_DIRECTORY
structure, it can have up to 16IMAGE_DATA_DIRECTORY
entry. This is a list of DataDirectory as defined inWinin.h
(Each value represents an index in the DataDirectory array):// Directory Entries #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- VirtualAddress: The RVA of the section.
- Size: The size of the section, in bytes.
-
Section headers
Before the sections come there is a lot of information that guides how to deal with the sections. the section headers consist of an array of the following structure:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
- Name: The name of the section.
- PhysicalAddress|VirtualSize: The total size of the section when loaded into memory, in bytes. If this value is greater than the SizeOfRawData member, the section is filled with zeroes.
- VirtualAddress: An RVA of the first byte of the section when loaded into memory.
- SizeOfRawData: The size of the initialized data on disk, in bytes. This value must be a multiple of the FileAlignment member of the
IMAGE_OPTIONAL_HEADER
structure. - PointerToRawData: A pointer to the first page of the section within the file. This value must be a multiple of the FileAlignment member of the
IMAGE_OPTIONAL_HEADER
structure. - PointerToRelocations: A pointer to the beginning of the relocation entries for the section. If there are no relocations, this value is zero.
- Characteristics: Flags that describe some information about the section like, is readable?, writable?, executable?, can be cached?, can be shared?, contains initialized or uninitialized data, and a lot more.
Parse headers
Let’s write a function that parses this data.
VOID ParseHeaders(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
WORD wValue, wNumberOfSections;
PRINT_LINE("IMAGE HEADERS", 100);
// Pointing now to the beging of the image
dwPtr = (DWORD_PTR) lpBaseAddress;
printf("Image magic number => 0x%X (%.2s)\n", ((PIMAGE_DOS_HEADER) dwPtr)->e_magic, (PCHAR) dwPtr);
printf("NT headers RVA => 0x%X\n", ((PIMAGE_DOS_HEADER) dwPtr)->e_lfanew);
// Move to the NT headers
dwPtr += ((PIMAGE_DOS_HEADER) dwPtr)->e_lfanew;
printf("NT headers signature => 0x%X (%s)\n", ((PIMAGE_NT_HEADERS) dwPtr)->Signature, (PCHAR) dwPtr);
//****************************************************************
PRINT_LINE("FILE HEADER", 100);
printf("Arch => ");
switch ( ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.Machine )
{
case IMAGE_FILE_MACHINE_I386:
puts("Intel x86");
break;
case IMAGE_FILE_MACHINE_IA64:
puts("Intel Itanium");
break;
case IMAGE_FILE_MACHINE_AMD64:
puts("AMD x64");
break;
default:
puts("UNKNOWN");
}
printf("Number of sections => %d\n", ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections);
printf("Size of optional headers => %d\n", ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.SizeOfOptionalHeader);
// Save number of sections to be reused later
wNumberOfSections = ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections;
//****************************************************************
PRINT_LINE("OPTIONAL HEADER", 100);
// Move to the optional headers
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader;
printf("The state of the image file => 0x%X\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->Magic);
printf("The entrypoint RVA => 0x%X\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->AddressOfEntryPoint);
printf("The size of the code => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfCode);
printf("The size of the initialized data => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfInitializedData);
printf("The size of the uninitialized data => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfUninitializedData);
printf("The size of the image => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfImage);
printf("The size of the headers => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfHeaders);
//****************************************************************
PRINT_LINE("DATA DIRECTORY", 100);
// Move to data directory
dwPtr = (DWORD_PTR) &((PIMAGE_OPTIONAL_HEADER) dwPtr)->DataDirectory;
// Number of directories
wValue = IMAGE_NUMBEROF_DIRECTORY_ENTRIES;
puts("Size\t\tRVA");
// Iterate over all directories
while ( wValue-- )
{
printf("%d\t\t0x%X\n", ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Move to the next entry
dwPtr += sizeof(IMAGE_DATA_DIRECTORY);
}
//****************************************************************
PRINT_LINE("SECTIONS", 100);
// AFTER OPTIONAL HEADERS COMES SECTION HEADERS, OUR dwPtr POINTS NOW TO THAT AREA
puts("Section Name\tSection RVA\tSize\tPtrToRawData RVA\tSizeOfRawData\tCharacteristics");
// Iterate over all sections
while ( wNumberOfSections-- )
{
printf("%.8s\t\t0x%X\t \t%d\t0x%X\t\t\t%d\t\t",
((PIMAGE_SECTION_HEADER) dwPtr)->Name,
((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress,
((PIMAGE_SECTION_HEADER) dwPtr)->Misc.VirtualSize,
((PIMAGE_SECTION_HEADER) dwPtr)->PointerToRawData,
((PIMAGE_SECTION_HEADER) dwPtr)->SizeOfRawData
);
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_READ )
printf("READABLE ");
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_WRITE )
printf("WRITABLE ");
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_EXECUTE )
printf("EXECUTABLE ");
// New line
puts("");
// Jump to the next section
dwPtr += sizeof(IMAGE_SECTION_HEADER);
}
}
PE Imports
This section is a very important section, to understand it we will go over the Data Directories located in .idata
.
Import Directory Table
The import directory table consists of an array of import directory entries, one entry for each DLL to which the image refers. The last directory entry is empty (filled with null values), which indicates the end of the directory table. Each import directory entry has the following:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
- Characteristics|OriginalFirstThunk: The RVA of the Import Lookup|Name Table (ILT | INT)
- TimeDateStamp: After the image is bound, this field is set to the time/data stamp of the DLL.
- ForwarderChain: The index of the first forwarder reference or -1 if no forwarders. (forwarding means the DLL redirect some of its exported functions to another DLL).
- Name: The RVA of an ASCII string that contains the name of the DLL.
- FirstThunk: The RVA of the Import Address Table (IAT)
Import Lookup Table
An import lookup table is an array of 32/64-bit numbers. The last entry is set to zero (NULL) to indicate the end of the table. Each entry uses the bit-field format as follows:
- The most significant bit (MSB) 31/63: If this bit is set, import by ordinal. Otherwise, import by name.
- Ordinal Number 0-15: A 16-bit ordinal number. This field is used only if the Ordinal/Name Flag bit field is 1 (import by ordinal).
- Hint/Name 30-0: A 31-bit RVA of a hint/name table entry. This field is used only if the Ordinal/Name Flag bit field is 0 (import by name).
Import Address Table
The structure and content of the import address table are identical to the import lookup table until the file is bound. During binding, the entries in the import address table are overwritten with the addresses of the symbols that are being imported. These addresses are the actual memory addresses of the symbols, although technically they are still called “virtual addresses.” The loader typically processes the binding.
Parse Imports
Let’s develop a function that parses this section and obtains its valuable data.
VOID ParseImports(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpImportLookupTable;
DWORD_PTR dwpImportAddressTable;
DWORD_PTR dwpImportByName;
PRINT_LINE("IMPORT SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Import Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_IMPORT;
// Return if there are no imports
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the import section (now pointing to import descriptors)
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Iterate over all import decriptors
do {
PRINT_LINE( (LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->Name), 50 );
// Get the ILT for current entry
dwpImportLookupTable = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->OriginalFirstThunk);
// Get the IAT for current entry
dwpImportAddressTable = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->FirstThunk);
// Iterate over all IAT and ILT
while ( DEREF(dwpImportAddressTable) )
{
// Check if the image imports by ordinal
if ( ((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
printf("\t- Ordinal => %d\n", IMAGE_ORDINAL(((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.Ordinal));
else {
dwpImportByName = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.AddressOfData);
printf("\t- Name => %s\n", (LPCSTR) ((PIMAGE_IMPORT_BY_NAME) dwpImportByName)->Name);
}
// Next imported function
dwpImportLookupTable += sizeof(DWORD_PTR);
dwpImportAddressTable += sizeof(DWORD_PTR);
}
// Jump to the next descriptor
dwPtr += sizeof(IMAGE_IMPORT_DESCRIPTOR);
} while ( ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->Name );
}
PE Exports
The export data section, named .edata, contains functions that have been exported and could be used by other programs that can be accessed through dynamic linking. Exported symbols are generally found in DLLs, but DLLs can also import symbols.
Export Directory Table
As described by Microsoft documentation, the export symbol information begins with the export directory table, which describes the remainder of the export symbol information.
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
- Name: The RVA of the ASCII string that contains the name of the DLL.
- NumberOfFunctions: The number of entries in the export address table.
- NumberOfNames: The number of entries in the name pointer table. This is also the number of entries in the ordinal table.
- AddressOfFunctions: The RVA of the export address table.
- AddressOfNames: The RVA of the export name pointer table.
- AddressOfNameOrdinals: The RVA of the ordinal table.
Export Address Table (EAT)
An array of the RVA of exported function addresses.
Export Name Table
An array of the RVA of the exported function names.
Export Ordinal Table
An array of the ordinal numbers, which are indexes of the function RVA within EAT, relative to its corresponding name. so if you want to retrieve the RVA of a function called SayHello
, and its index inside Export Name Table is 0, you can retrieve its index inside Export Address Table via Export Ordinal Table by name index which is zero.
Parse Exports
Let’s develop a function that parses this section and obtains its valuable data.
VOID ParseExports(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpAddress;
DWORD_PTR dwpNames;
DWORD_PTR dwpOrdinals;
DWORD dwValue;
PRINT_LINE("EXPORT SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Export Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_EXPORT;
// Check if there are no exports
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the export section
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
printf("DLL name => %s\n", (LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->Name));
printf("Number of exported functions => %d\n", ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->NumberOfFunctions);
// Get Addresses of names, ordinals
dwpNames = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfNames);
dwpOrdinals = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfNameOrdinals);
// Get number of names
dwValue = ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->NumberOfNames;
puts("\nName RVA\tAddress RVA\tOrdinal\tAddress\t\tName");
// Iterate over all EAT, Exported names, and ordinals
while ( dwValue-- )
{
// Jump to EAT
dwpAddress = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfFunctions);
// Retrieve current function address by name ordinal
dwpAddress += sizeof(DWORD_PTR) * DEREF16(dwpOrdinals);
// Display entry information
printf("0x%X\t\t0x%X\t\t0x%X\t0x%X\t%s\n",
DEREF32(dwpNames), // Name RVA
DEREF32(dwpAddress), // Address RVA
DEREF16(dwpOrdinals), // Ordinal
(DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, DEREF32(dwpAddress)), // Function Address
(LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, DEREF32(dwpNames)) // Function Name
);
// Next exported function
dwpNames += sizeof(DWORD);
dwpOrdinals += sizeof(WORD);
}
}
PE Relocations
When the image is compiled, the compiler assumes that the executable will be loaded at a specific address, which is saved in the image’s headers, as seen in IMAGE_OPTIONAL_HEADER.ImageBase
. Based on this address, some addresses are calculated and hardcoded into the image. The problem arises because the loader is not limited to this hardcoded base address, it can load the image elsewhere, making the other addresses invalid. so those addresses need fixup which is the loader’s responsibility. To understand the fixup process let’s assume that the hardcoded based address is 0x800000 and we have a function whose RVA is 0x800, then the function’s address is 0x800800, but what if the image gets loaded into 0x808000, yep the function’s address becomes invalid. For this issue to be fixed the loader overwrites the Relocation Blocks by calculating the delta between the actual memory address and the hardcoded address, so the loader calculates it as follows 0x808000 - 0x800000 = 0x8000
, then the loader adds this value to the old function’s address 0x800800 + 0x8000
gives us the correct address which is 0x808800.
Base Relocation Table/Blocks
The base relocation table contains entries for all base relocations in the image.
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
- VirtualAddress: The image base plus the page RVA is added to each offset to create the VA where the base relocation must be applied.
- SizeOfBlock: The total number of bytes in the base relocation block, including the Page RVA and Block Size fields and the Type/Offset fields that follow.
Parse Relocations
Let’s develop a function that parses this section and obtains its data.
VOID ParseRelocations(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpEntry;
DWORD dwNumberOfEntries;
PRINT_LINE("RELOCATION SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Relocation Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_BASERELOC;
// Return if there are no relocations
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the reloc section
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Iterate over all relocation blocks
while ( ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock )
{
// Calculate the number of entries inside this block
dwNumberOfEntries = ( ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof(RELOC_ENTRY);
// Display block information
printf("Page RVA = 0x%X, Block Size = %d, Number Of Entries = %d\n",
((PIMAGE_BASE_RELOCATION) dwPtr)->VirtualAddress,
((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock,
dwNumberOfEntries
);
// Pointing to the first entry in current block
dwpEntry = dwPtr + sizeof(IMAGE_BASE_RELOCATION);
// Iterate over all entries
while ( dwNumberOfEntries-- )
{
printf("\tOffset => 0x%X\n\tType => 0x%X\n\n",
((PRELOC_ENTRY) dwpEntry)->w12Offset,
((PRELOC_ENTRY) dwpEntry)->w4Type
);
// Next entry
dwpEntry += sizeof(RELOC_ENTRY);
}
// Next relocation block
dwPtr += ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock;
}
}
The full code
#include <Windows.h>
#include <stdio.h>
#define PRINT_LINE(str, n) \
printf("\n------%s", str); \
for (WORD i = 0; i < n - strlen(str); i++) \
printf("-"); \
puts("")
// Dereferencing macros
#define DEREF(addr)*(DWORD_PTR *)(addr)
#define DEREF16(addr)*(WORD *)(addr)
#define DEREF32(addr)*(DWORD *)(addr)
typedef struct _RELOC_ENTRY {
WORD w12Offset : 12;
WORD w4Type : 4;
} RELOC_ENTRY, * PRELOC_ENTRY;
DWORD ResolveOffset(LPVOID lpBaseAddress, DWORD dwRVA)
{
DWORD_PTR dwPtr;
DWORD dwNumberOfSections;
// Nt Headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Retrieve number of sections from the file header
dwNumberOfSections = ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections;
// Jump to the section header
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader + ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.SizeOfOptionalHeader;
// Iterate over all sections header
while ( dwNumberOfSections-- )
{
// Check if the given RVA in the range of this section
if (
dwRVA >= ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress &&
dwRVA < ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress + ((PIMAGE_SECTION_HEADER) dwPtr)->SizeOfRawData
)
// Calculate the file offset
return dwRVA - ((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress + ((PIMAGE_SECTION_HEADER) dwPtr)->PointerToRawData;
// Next section header
dwPtr += sizeof(IMAGE_SECTION_HEADER);
}
// Invalid RVA
return 0;
}
VOID ParseHeaders(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
WORD wValue, wNumberOfSections;
PRINT_LINE("IMAGE HEADERS", 100);
// Pointing now to the beging of the image
dwPtr = (DWORD_PTR) lpBaseAddress;
printf("Image magic number => 0x%X (%.2s)\n", ((PIMAGE_DOS_HEADER) dwPtr)->e_magic, (PCHAR) dwPtr);
printf("NT headers RVA => 0x%X\n", ((PIMAGE_DOS_HEADER) dwPtr)->e_lfanew);
// Move to the NT headers
dwPtr += ((PIMAGE_DOS_HEADER) dwPtr)->e_lfanew;
printf("NT headers signature => 0x%X (%s)\n", ((PIMAGE_NT_HEADERS) dwPtr)->Signature, (PCHAR) dwPtr);
//****************************************************************
// Processing file header
PRINT_LINE("FILE HEADER", 100);
printf("Arch => ");
switch ( ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.Machine )
{
case IMAGE_FILE_MACHINE_I386:
puts("Intel x86");
break;
case IMAGE_FILE_MACHINE_IA64:
puts("Intel Itanium");
break;
case IMAGE_FILE_MACHINE_AMD64:
puts("AMD x64");
break;
default:
puts("UNKNOWN");
}
printf("Number of sections => %d\n", ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections);
printf("Size of optional headers => %d\n", ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.SizeOfOptionalHeader);
// Save number of sections to be reused later
wNumberOfSections = ((PIMAGE_NT_HEADERS) dwPtr)->FileHeader.NumberOfSections;
//****************************************************************
PRINT_LINE("OPTIONAL HEADER", 100);
// Move to the optional headers
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader;
printf("The state of the image file => 0x%X\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->Magic);
printf("The entrypoint RVA => 0x%X\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->AddressOfEntryPoint);
printf("The size of the code => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfCode);
printf("The size of the initialized data => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfInitializedData);
printf("The size of the uninitialized data => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfUninitializedData);
printf("The size of the image => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfImage);
printf("The size of the headers => %d\n", ((PIMAGE_OPTIONAL_HEADER) dwPtr)->SizeOfHeaders);
//****************************************************************
PRINT_LINE("DATA DIRECTORY", 100);
// Move to data directory
dwPtr = (DWORD_PTR) &((PIMAGE_OPTIONAL_HEADER) dwPtr)->DataDirectory;
// Number of directories
wValue = IMAGE_NUMBEROF_DIRECTORY_ENTRIES;
puts("Size\t\tRVA");
// Iterate over all directories
while ( wValue-- )
{
printf("%d\t\t0x%X\n", ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Move to the next entry
dwPtr += sizeof(IMAGE_DATA_DIRECTORY);
}
//****************************************************************
PRINT_LINE("SECTIONS", 100);
// AFTER OPTIONAL HEADERS COMES SECTION HEADERS, OUR dwPtr POINTS NOW TO THAT AREA
puts("Section Name\tSection RVA\tSize\tPtrToRawData RVA\tSizeOfRawData\tCharacteristics");
// Iterate over all sections
while ( wNumberOfSections-- )
{
printf("%.8s\t\t0x%X\t \t%d\t0x%X\t\t\t%d\t\t",
((PIMAGE_SECTION_HEADER) dwPtr)->Name,
((PIMAGE_SECTION_HEADER) dwPtr)->VirtualAddress,
((PIMAGE_SECTION_HEADER) dwPtr)->Misc.VirtualSize,
((PIMAGE_SECTION_HEADER) dwPtr)->PointerToRawData,
((PIMAGE_SECTION_HEADER) dwPtr)->SizeOfRawData
);
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_READ )
printf("READABLE ");
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_WRITE )
printf("WRITABLE ");
if ( ((PIMAGE_SECTION_HEADER) dwPtr)->Characteristics & IMAGE_SCN_MEM_EXECUTE )
printf("EXECUTABLE ");
// New line
puts("");
// Jump to the next section
dwPtr += sizeof(IMAGE_SECTION_HEADER);
}
}
VOID ParseImports(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpImportLookupTable;
DWORD_PTR dwpImportAddressTable;
DWORD_PTR dwpImportByName;
PRINT_LINE("IMPORT SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Import Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_IMPORT;
// Return if there are no imports
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the import section (now pointing to import descriptors)
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Iterate over all import decriptors
do {
PRINT_LINE( (LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->Name), 50 );
// Get the ILT for current entry
dwpImportLookupTable = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->OriginalFirstThunk);
// Get the IAT for current entry
dwpImportAddressTable = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->FirstThunk);
// Iterate over all IAT and ILT
while ( DEREF(dwpImportAddressTable) )
{
// Check if the image imports by ordinal
if ( ((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
printf("\t- Ordinal => %d\n", IMAGE_ORDINAL(((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.Ordinal));
else {
dwpImportByName = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_THUNK_DATA) dwpImportLookupTable)->u1.AddressOfData);
printf("\t- Name => %s\n", (LPCSTR) ((PIMAGE_IMPORT_BY_NAME) dwpImportByName)->Name);
}
// Next imported function
dwpImportLookupTable += sizeof(DWORD_PTR);
dwpImportAddressTable += sizeof(DWORD_PTR);
}
// Jump to the next descriptor
dwPtr += sizeof(IMAGE_IMPORT_DESCRIPTOR);
} while ( ((PIMAGE_IMPORT_DESCRIPTOR) dwPtr)->Name );
}
VOID ParseExports(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpAddress;
DWORD_PTR dwpNames;
DWORD_PTR dwpOrdinals;
DWORD dwValue;
PRINT_LINE("EXPORT SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Export Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_EXPORT;
// Check if there are no exports
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the export section
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
printf("DLL name => %s\n", (LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->Name));
printf("Number of exported functions => %d\n", ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->NumberOfFunctions);
// Get Addresses of names, ordinals
dwpNames = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfNames);
dwpOrdinals = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfNameOrdinals);
// Get number of names
dwValue = ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->NumberOfNames;
puts("\nName RVA\tAddress RVA\tOrdinal\tAddress\t\tName");
// Iterate over all EAT, Exported names, and ordinals
while ( dwValue-- )
{
// Jump to EAT
dwpAddress = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_EXPORT_DIRECTORY) dwPtr)->AddressOfFunctions);
// Retrieve current function address by name ordinal
dwpAddress += sizeof(DWORD_PTR) * DEREF16(dwpOrdinals);
// Display entry information
printf("0x%X\t\t0x%X\t\t0x%X\t0x%X\t%s\n",
DEREF32(dwpNames), // Name RVA
DEREF32(dwpAddress), // Address RVA
DEREF16(dwpOrdinals), // Ordinal
(DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, DEREF32(dwpAddress)), // Function Address
(LPCSTR) lpBaseAddress + ResolveOffset(lpBaseAddress, DEREF32(dwpNames)) // Function Name
);
// Next exported function
dwpNames += sizeof(DWORD);
dwpOrdinals += sizeof(WORD);
}
}
VOID ParseRelocations(LPVOID lpBaseAddress)
{
DWORD_PTR dwPtr;
DWORD_PTR dwpEntry;
DWORD dwNumberOfEntries;
PRINT_LINE("RELOCATION SECTION", 100);
// Move to NT headers
dwPtr = (DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew;
// Move to Data Directory
dwPtr = (DWORD_PTR) &((PIMAGE_NT_HEADERS) dwPtr)->OptionalHeader.DataDirectory;
// Now pointing to Relocation Directory
dwPtr += sizeof(IMAGE_DATA_DIRECTORY) * IMAGE_DIRECTORY_ENTRY_BASERELOC;
// Return if there are no relocations
if ( ((PIMAGE_DATA_DIRECTORY) dwPtr)->Size == 0 ) return;
// Jump to the reloc section
dwPtr = (DWORD_PTR) lpBaseAddress + ResolveOffset(lpBaseAddress, ((PIMAGE_DATA_DIRECTORY) dwPtr)->VirtualAddress);
// Iterate over all relocation blocks
while ( ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock )
{
// Calculate the number of entries inside this block
dwNumberOfEntries = ( ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof(RELOC_ENTRY);
// Display block information
printf("Page RVA = 0x%X, Block Size = %d, Number Of Entries = %d\n",
((PIMAGE_BASE_RELOCATION) dwPtr)->VirtualAddress,
((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock,
dwNumberOfEntries
);
// Pointing to the first entry in current block
dwpEntry = dwPtr + sizeof(IMAGE_BASE_RELOCATION);
// Iterate over all entries
while ( dwNumberOfEntries-- )
{
printf("\tOffset => 0x%X\n\tType => 0x%X\n\n",
((PRELOC_ENTRY) dwpEntry)->w12Offset,
((PRELOC_ENTRY) dwpEntry)->w4Type
);
// Next entry
dwpEntry += sizeof(RELOC_ENTRY);
}
// Next relocation block
dwPtr += ((PIMAGE_BASE_RELOCATION) dwPtr)->SizeOfBlock;
}
}
BOOL ParsePE(LPVOID lpBaseAddress)
{
// Check if a valid PE
if ( !(
// Check the DOS header signature
( ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_magic == IMAGE_DOS_SIGNATURE ) &&
// Check if the RVA of the NT headers in the expected range
( ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew >= sizeof(IMAGE_DOS_HEADER) && ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew < 0x400 ) &&
// Check the NT header signature
( ((PIMAGE_NT_HEADERS)((DWORD_PTR) lpBaseAddress + ((PIMAGE_DOS_HEADER) lpBaseAddress)->e_lfanew))->Signature == IMAGE_NT_SIGNATURE )
))
return FALSE;
ParseHeaders(lpBaseAddress);
ParseImports(lpBaseAddress);
ParseExports(lpBaseAddress);
ParseRelocations(lpBaseAddress);
return TRUE;
}
LPVOID ReadPEFile(char *cpFileName)
{
HANDLE hFile = NULL;
LPVOID lpBuffer = NULL;
DWORD dwSize, dwRead;
// Get a handle on the file with some attributes.
hFile = CreateFileA( cpFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
// If the given PE file doesn't exist, return NULL
if ( hFile == INVALID_HANDLE_VALUE )
goto LEAVE;
dwSize = GetFileSize(hFile, NULL);
// If the given PE file was empty or invalid, close the file handle, return NULL
if ( dwSize == INVALID_FILE_SIZE || dwSize == 0 )
goto LEAVE;
// Allocate sufficient memory for the PE in the main heap
lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwSize);
// If allocation request failed, return NULL
if ( ! lpBuffer )
goto LEAVE;
// Read the PE file in the allocated memory
if ( ! ReadFile(hFile, lpBuffer, dwSize, &dwRead, NULL) )
{
// If we failed to read
// Deallocate the memory, close the file handle, and return NULL
HeapFree(GetProcessHeap(), 0, lpBuffer);
lpBuffer = NULL;
}
LEAVE:
if ( hFile ) CloseHandle(hFile);
return lpBuffer;
}
int main(int argc, char **argv)
{
LPVOID lpBuffer;
int nRet = EXIT_FAILURE;
if ( argc <= 1 )
return puts("Usage:\n\t./PEParser </path/to/pefile>");
lpBuffer = ReadPEFile(argv[1]);
if ( !lpBuffer )
{
printf("Failed to read %s\n", argv[1]);
goto LEAVE;
}
if ( ! ParsePE(lpBuffer) )
{
puts("Invalid PE file");
goto LEAVE;
}
nRet = EXIT_SUCCESS;
LEAVE:
if ( lpBuffer ) HeapFree(GetProcessHeap(), 0, lpBuffer);
return nRet;
}
Let’s parse AMSI DLL and see the results.
C:\path\to>PEParser.exe c:\Windows\System32\amsi.dll
------IMAGE HEADERS---------------------------------------------------------------------------------------
Image magic number => 0x5A4D (MZ)
NT headers RVA => 0xF8
NT headers signature => 0x4550 (PE)
------FILE HEADER-----------------------------------------------------------------------------------------
Arch => Intel x86
Number of sections => 6
Size of optional headers => 224
------OPTIONAL HEADER-------------------------------------------------------------------------------------
The state of the image file => 0x10B
The entrypoint RVA => 0xED00
The size of the code => 65024
The size of the initialized data => 18944
The size of the uninitialized data => 0
The size of the image => 102400
The size of the headers => 1024
------DATA DIRECTORY--------------------------------------------------------------------------------------
Size RVA
396 0x10BC0
480 0x12204
5096 0x15000
0 0x0
0 0x0
4452 0x17000
84 0x3460
0 0x0
0 0x0
0 0x0
172 0x1698
0 0x0
512 0x12000
128 0x109E0
0 0x0
0 0x0
------SECTIONS--------------------------------------------------------------------------------------------
Section Name Section RVA Size PtrToRawData RVA SizeOfRawData Characteristics
.text 0x1000 64844 0x400 65024 READABLE EXECUTABLE
.data 0x11000 3636 0x10200 2048 READABLE WRITABLE
.idata 0x12000 4294 0x10A00 4608 READABLE
.didat 0x14000 56 0x11C00 512 READABLE WRITABLE
.rsrc 0x15000 5096 0x11E00 5120 READABLE
.reloc 0x17000 4452 0x13200 4608 READABLE
------IMPORT SECTION--------------------------------------------------------------------------------------
------msvcrt.dll----------------------------------------
- Name => memcpy_s
- Name => memmove
- Name => srand
- Name => memcpy
- Name => _CxxThrowException
- Name => ?what@exception@@UBEPBDXZ
- Name => ??0exception@@QAE@ABQBDH@Z
- Name => ??0exception@@QAE@ABQBD@Z
- Name => _callnewh
- Name => rand
- Name => memmove_s
- Name => _purecall
- Name => _vsnprintf_s
- Name => memcmp
- Name => wcsnlen
- Name => ??1type_info@@UAE@XZ
- Name => _onexit
- Name => __dllonexit
- Name => _unlock
- Name => ??0exception@@QAE@XZ
- Name => _lock
- Name => ?terminate@@YAXXZ
- Name => _except_handler4_common
- Name => ??3@YAXPAX@Z
- Name => _vsnwprintf
- Name => _initterm
- Name => _amsg_exit
- Name => ??_V@YAXPAX@Z
- Name => _XcptFilter
- Name => __CxxFrameHandler3
- Name => malloc
- Name => ??1exception@@UAE@XZ
- Name => ??0exception@@QAE@ABV0@@Z
- Name => free
- Name => time
- Name => memset
------api-ms-win-core-synch-l1-1-0.dll------------------
- Name => ReleaseSRWLockShared
- Name => DeleteCriticalSection
- Name => OpenSemaphoreW
- Name => AcquireSRWLockExclusive
- Name => WaitForSingleObjectEx
- Name => ReleaseSRWLockExclusive
- Name => WaitForSingleObject
- Name => ReleaseSemaphore
- Name => CreateSemaphoreExW
- Name => AcquireSRWLockShared
- Name => CreateMutexExW
- Name => ReleaseMutex
- Name => InitializeCriticalSection
- Name => LeaveCriticalSection
- Name => EnterCriticalSection
- Name => InitializeCriticalSectionEx
------api-ms-win-eventing-provider-l1-1-0.dll-----------
- Name => EventSetInformation
- Name => EventProviderEnabled
- Name => EventRegister
- Name => EventUnregister
- Name => EventWriteTransfer
- Name => EventWrite
------api-ms-win-eventing-classicprovider-l1-1-0.dll----
- Name => GetTraceLoggerHandle
- Name => GetTraceEnableFlags
- Name => UnregisterTraceGuids
- Name => RegisterTraceGuidsW
- Name => GetTraceEnableLevel
- Name => TraceMessage
------api-ms-win-core-errorhandling-l1-1-0.dll----------
- Name => UnhandledExceptionFilter
- Name => SetUnhandledExceptionFilter
- Name => SetLastError
- Name => GetLastError
------api-ms-win-core-libraryloader-l1-2-0.dll----------
- Name => GetProcAddress
- Name => GetModuleHandleExW
- Name => GetModuleFileNameA
- Name => LoadLibraryExW
- Name => GetModuleHandleW
------api-ms-win-core-heap-l1-1-0.dll-------------------
- Name => HeapFree
- Name => HeapAlloc
- Name => GetProcessHeap
------api-ms-win-core-processthreads-l1-1-0.dll---------
- Name => TerminateProcess
- Name => GetCurrentThreadId
- Name => GetCurrentProcessId
- Name => GetCurrentProcess
------api-ms-win-core-localization-l1-2-0.dll-----------
- Name => FormatMessageW
------api-ms-win-core-debug-l1-1-0.dll------------------
- Name => DebugBreak
- Name => OutputDebugStringW
- Name => IsDebuggerPresent
------api-ms-win-core-synch-l1-2-0.dll------------------
- Name => Sleep
------api-ms-win-core-profile-l1-1-0.dll----------------
- Name => QueryPerformanceCounter
------api-ms-win-core-sysinfo-l1-1-0.dll----------------
- Name => GetTickCount
- Name => GetSystemTimeAsFileTime
------RPCRT4.dll----------------------------------------
- Name => UuidFromStringW
------api-ms-win-core-registry-l1-1-0.dll---------------
- Name => RegCloseKey
- Name => RegOpenKeyExW
- Name => RegEnumKeyExW
- Name => RegQueryInfoKeyW
- Name => RegGetValueW
------api-ms-win-core-sysinfo-l1-2-0.dll----------------
- Name => GetSystemTimePreciseAsFileTime
------api-ms-win-core-threadpool-l1-2-0.dll-------------
- Name => SetThreadpoolTimer
- Name => CloseThreadpoolTimer
- Name => CreateThreadpoolTimer
- Name => WaitForThreadpoolTimerCallbacks
------api-ms-win-core-file-l1-1-0.dll-------------------
- Name => CreateFileW
------api-ms-win-core-processthreads-l1-1-1.dll---------
- Name => OpenProcess
------api-ms-win-core-handle-l1-1-0.dll-----------------
- Name => CloseHandle
------api-ms-win-core-delayload-l1-1-1.dll--------------
- Name => ResolveDelayLoadedAPI
------api-ms-win-core-delayload-l1-1-0.dll--------------
- Name => DelayLoadFailureHook
------ntdll.dll-----------------------------------------
- Name => NtQueryInformationProcess
------EXPORT SECTION--------------------------------------------------------------------------------------
DLL name => Amsi.dll
Number of exported functions => 13
Name RVA Address RVA Ordinal Address Name
0x10C73 0x59D0 0x0 0x52AD90 AmsiCloseSession
0x10C84 0x56B0 0x1 0x52AA70 AmsiInitialize
0x10C93 0x5970 0x2 0x52AD30 AmsiOpenSession
0x10CA3 0x5A00 0x3 0x52ADC0 AmsiScanBuffer
0x10CB2 0x5AB0 0x4 0x52AE70 AmsiScanString
0x10CC1 0x5B00 0x5 0x52AEC0 AmsiUacInitialize
0x10CD3 0x5D20 0x6 0x52B0E0 AmsiUacScan
0x10CDF 0x5CD0 0x7 0x52B090 AmsiUacUninitialize
0x10CF3 0x5920 0x8 0x52ACE0 AmsiUninitialize
0x10D04 0x4600 0x9 0x5299C0 DllCanUnloadNow
0x10D14 0x4630 0xA 0x5299F0 DllGetClassObject
0x10D26 0x4660 0xB 0x529A20 DllRegisterServer
0x10D38 0x4660 0xC 0x529A20 DllUnregisterServer
------RELOCATION SECTION----------------------------------------------------------------------------------
Page RVA = 0x1000, Block Size = 952, Number Of Entries = 472
Offset => 0x0
Type => 0x3
Offset => 0x4
Type => 0x3
Offset => 0x8
Type => 0x3
Offset => 0xC
Type => 0x3
Offset => 0x10
Type => 0x3
Offset => 0x14
Type => 0x3
Offset => 0x18
Type => 0x3
Offset => 0x24
Type => 0x3
Offset => 0x2C
Type => 0x3
Offset => 0x34
Type => 0x3
Offset => 0x3C
Type => 0x3
Offset => 0x44
Type => 0x3
.
..
...
..... (A LOT OF ENTRIES)
Conclusion
Try playing with tools such as dumpbin and PEBear and develop your parser to solidify your understanding, I know the topic is a little bit complicated, but practice always is key. Thank you for joining me on this exploration.