This article is just a small summary regarding structures of dynamic functions import or export in 32 bits portable executable binary files. The aim is not to be comprehensive or easy to understand but small and quick to summarize.
1 A multi-byte value such as dword or word is coded using little endianess, which means that in the file data as well as in memory, the bytes order as reversed: 0x01020304 is stored as 04 03 02 01 in memory.
In the portable executable format, a header – the PE header – is defined at the begin of the file. The position of the PE header is indicated at the file offset 0x3C by a dword and is composed by a static part followed by an optional part: the optional header.
In the PE header, at relative offset +20 bytes, a word value indicates the size of the Optional header.
If the Optional header is classical – all fields with 16 Data Directory entries – then, this value is 0x00E0.
The Data directory table is an array of NumberOfDirectoryEntries × 2 dword values, at relative position +120 bytes from PE header.
So this array is composed by a couple of cells (most of time 16), themselves composed by two dword: Address (rva) to data and data Size.
struct DirectoryEntry { dword Address; // rva dword Size; } DirectoryEntry DataDirectory[NumberOfDirectoryEntries];
As Address is relative, to compute absolute address, it's needed to add the imagebase of the module.
The imagebase is specified as dword value at relative address +52 from the PE header.
For each cell corresponds to a directory (see array bellow). If a directory is not used, the Address and the Size are null.
0 | Export Table |
1 | Import Table |
2 | Resource Table |
3 | Exception Table |
4 | Certificate File |
5 | Relocation Table |
6 | Debug Data |
7 | Architecture Data |
8 | Global Ptr |
9 | Thread Local Storage Table |
10 | Load Config Table |
11 | Bound Import Table |
12 | Import Address Table |
13 | Delay Import Descriptor |
14 | COM+ Runtime Header |
15 | Reserved |
Here, we see that Export table is DataDirectory[0] while Import table is DataDirectory[1]. Note also the existence of Import Address Table – DataDirectory[12] – which will be explained after.
The directory Import Table points to a non-length-fixed array for which each cell is a structure of 5 dword values.
The directory Export Table points to a structure of 10 dword values.
This array has a dynamic size. To length of the array is not known, we need to browse each cell until the terminal cell.
The terminal cell has the Module value defined to null. Then we know the last cell is reached, this last cell which is of course ignored.
The structure corresponding to a cell has two different design, whose I call Borland design or Microsoft design
struct functionEntry { word Hint; // ordinal number of function, null if unused byte Name[]; // asciiz } struct importEntry { dword Functions; // null dword Timestamp; // not interesting dword Forwad; // idem dword Module; // points (rva) to the asciiz module name of dll dword Thunk; // in the file, points (rva) to an array of dword pointers (rva) ponting to a functionEntry. // Windows loader will overwrite pointers to points functionEntry by loaded functions address. 2,3 } importEntry importTable[];
2 The loader overwrite data of this array (in red, above) with functions address.
If this program is dumped from memory to a file, an Import Table reconstruction is needed to restore original rva pointer to functionEntry.
struct functionEntry { word Hint; // ordinal number of function, null if unused byte Name[]; // asciiz } struct importEntry { dword Functions; // points (rva) to an array of dword pointers (rva) ponting to a functionEntry dword Timestamp; // not interesting dword Forwad; // idem dword Module; // points (rva) to the asciiz module name of dll dword Thunk; // points (rva) to an array of pointers (rva) pointing to functions address destination. This array is filled by the loader of Windows. 3 } importEntry importTable[];
3 If these arrays are contiguously located in memory for all import libraries, then this zone should be pointed by the Import Address Table, in the Directory data table.
An exported function can be called using its name or its Hint ordinal number.
struct { dword Reserved; // not used dword Timestamp; // not interestting dword Version; // idem dword Module; // points (rva) to asciiz name of module. dword Base; // first ordinal attributed to a function (most of time, 1) dword NumberOfFunctions; // number of exported functions dword NumberOfNames; // number of exported functions dword AddressOfFunctions; // points (rva) to an array of dword pointers to code functions. dword AddressOfNames; // points (rva) to an array of dword pointers (rva) to name of exported functions. dword AddressOfOrdinals; // points (rva) to an array of word values corresponding to ordinal (unique number) of functions. }
Values NumberOfFunctions and NumberOfNames are identicals.
This value defines the number of elements of the three arrays AddressOfFunctions, AddressOfNames and AddressOfOrdinals.