0xNinjaCyclone Blog

Penetration tester and Red teamer


[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:

    1. Iterate over all sections to determine which section the RVA lives in.
    2. 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 to IMAGE_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 16 IMAGE_DATA_DIRECTORY entry. This is a list of DataDirectory as defined in Winin.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.