Simple Example of Export Directory
Let’s take a look at a simple example to understand how the export directory is used by the executables/libraries. Let’s suppose that we’re dealing with a .dll library that has 10 exported functions, so the NumberOfFunctions=10. It has 5 names, so the NumberOfNames=5. Because it has 5 names, the number of elements in the AddressOfNames and AddressOfNameOrdinals is also 5. The picture below shows this example:
In the picture above, we can see that the AddressOfFunctions array holds the addresses of all 10 exported functions. The addresses are denoted with addr0…addr9. The AddressOfNames contains 5 names that correspond to certain addresses. Remember that there is no indication which name links to which address in the AddressOfNames array. We showed the arrows just so we can better illustrate the example. The linkage between the names and their appropriate addresses is written in the AddressOfNameOrdinals array. Each element of that array holds an index of the corresponding elements from the AddressOfNames array to the element in AddressOfFunctions array. The name4 function is the fifth name in the AddressOfNames array, which is why the fifth element in the AddressOfNameOrdinals array will hold the index of the corresponding address of that function in the AddressOfFunctions array.
There are two scenarios we need to go over. The first one is when the function is exported by name and we must find its address. The second one is when the function is exported by ordinal, and we must also find the function’s address. When we want to figure out the address of the function exported by name, we have to traverse the AddressOfNames and AddressOfNameOrdinals at the same time. When we find a matching name in the AddressOfNames array, we must take the number at the same index from the AddressOfNameOrdinals array. The extracted number is the index into the AddressOfFunctions array where we can get the RVA of the associated function. When the function is exported by an ordinal, we can directly use the ordinal number as an index into the AddressOfFunctions array if the Base number is 0. In our case, the Base number is 1, which means that we have to take the ordinal number and subtract the Base number (which is 1 in this case) from it. This makes a lot of sense, because the ordinal numbers start counting from 1 onwards, but in C arrays we’re always starting to count with 0. If we start the PE Explorer tool and open the kernel32.dll, we can see that the ordinal numbers start with 1 and not 0:
We can see that when we’re using the ordinals, obtaining the address of the function is much faster because we only have to calculate one subtract operation, while with names we have to traverse and compare each and every name in the AddressOfNames array. But by using the ordinals, the whole process is not very compatible over the systems, because the ordinal numbers change if we update or edit the library; the libraries don’t get updated so often, but it does happen when the libraries must be updated because of the bug or because the developer’s added new features. In this case, if ordinals were used, we would effectively be calling the wrong function.
Complex Example of Export Directory
Let’s dump the file headers of the kernel32.dll library. This can be seen in the output below:
0:002> !dh 7c800000 -f File Type: DLL FILE HEADER VALUES 14C machine (i386) 4 number of sections 506BC5E5 time date stamp Wed Oct 03 06:58:13 2012 0 file pointer to symbol table 0 number of symbols E0 size of optional header 210E characteristics Executable Line numbers stripped Symbols stripped 32 bit word machine DLL OPTIONAL HEADER VALUES 10B magic # 7.10 linker version 83400 size of code 70400 size of initialized data 0 size of uninitialized data B64E address of entry point 1000 base of code ----- new ----- 7c800000 image base 1000 section alignment 200 file alignment 3 subsystem (Windows CUI) 5.01 operating system version 5.01 image version 4.00 subsystem version F6000 size of image 400 size of headers FBCBC checksum 00040000 size of stack reserve 00001000 size of stack commit 00100000 size of heap reserve 00001000 size of heap commit 0 DLL characteristics 262C [ 6D19] address [size] of Export Directory 8190C [ 28] address [size] of Import Directory 8A000 [ 65EE8] address [size] of Resource Directory 0 [ 0] address [size] of Exception Directory 0 [ 0] address [size] of Security Directory F0000 [ 5C8C] address [size] of Base Relocation Directory 841FC [ 38] address [size] of Debug Directory 0 [ 0] address [size] of Description Directory 0 [ 0] address [size] of Special Directory 0 [ 0] address [size] of Thread Storage Directory 4E698 [ 40] address [size] of Load Configuration Directory 0 [ 0] address [size] of Bound Import Directory 1000 [ 624] address [size] of Import Address Table Directory 0 [ 0] address [size] of Delay Import Directory 0 [ 0] address [size] of COR20 Header Directory 0 [ 0] address [size] of Reserved Directory
This time, we’re interested in the export directory that starts at RVA 0x262C, which means that the actual address is 0x7c800000+0x262C = 0x7c80262c. We also know that export directory uses the _IMAGE_EXPORT_DIRECTORY structure to present the data. The picture below dumps the memory where the export table is located:
We used the _IMAGE_EXPORT_DIRECTORY structure to dump the memory, which contains a number of fields seen above. The structure is contained in the winnt.h header file and has the following definition:
We can see that we’ve actually printed those exact elements on the previous picture. Let’s now explain the important elements of the IMAGE_EXPORT_DIRECTORY structure:
- Characteristics [32 bits]: unused.
- TimeDateStamp [32 bits]: the time the table was created (some linkers set it to 0)
- MajorVersion [16 bits]: often 0
- MinorVersion [16 bits]: often 0
- Name [32 bits]: the name of the DLL
- Base [32 bits]: the number used to subtract from the ordinal number to get the index into the AddressOfFunctions array.
- NumberOfFunctions [32 bits]: total number of exported functions, either by name or ordinal.
- NumberOfNames [32 bits]: number of exported names, which need not be the NumberOfFunctions that presents all of the functions exported by module. Rather than that, this field presents only the number of functions exported by name. Functions can also be exported by ordinal, rather than name. If this value is 0, then all of the functions in this module are exported by ordinal and none of them is exported by name.
- AddressOfFunctions [32 bits]: a RVA to the list of exported functions – it points to an array of NumberOfFunctions 32-bit values, each being a RVA to the exported function or variable.
- AddressOfNames [32 bits]: a RVA to the list of exported names – it points to an array of NumberOfNames 32-bit values, each being a RVA to the exported symbol name.
- AddressOfNameOrdinals [16 bits]: a RVA to the list of ordinals – it points to an array of NumberOfNames 16-bit values, each being an ordinal.
We need to understand that each library needs to keep the information about the imported/exported functions somewhere in PE header. How else would the operating system find it when needed?
The Name file of the IMAGE_EXPORT_DIRECTORY structure holds the RVA address to the name of the library we’re currently analyzing. In our case, the RVA address is 0x4b98, which means the whole address where the name of the current library is saved is 0x7c800000+0x4b98. We can print the string located at that address with the da command as shown on the picture below:
We can see that we’re currently analyzing kernel32.dll library, which is correct. Let’s now dump NumberOfFunctions and NumberOfNames that present the total number of exported functions and the number of exported functions by name. In our case, both numbers are 0x3ba, which means that all of the exported functions in kernel32.dll are exported by name, and none of them is exported by the original. The 0x3ba hexadecimal number is 954 in decimal, which means that kernel32.dll exports 954 function names in total.
Let’s now dump the first eight elements of AddressOfFunctions array. Since the RVA of AddressOfFunctions element is 0×2654, we must dump the memory at address 0x7c800000+0×2654. On the picture below, we used the dd command to dump the memory at the virtual address of the AddressOfFunctions array.
We dumped only the first eight addresses. To get the real virtual addresses, we must add 0x7c800000 to the obtained RVAs: 0xa6e4, 0x145cd, …, 0x2d639. The first eight exported functions of the kernel32.dll library use the following addresses:
|Function Address||Function Name|
Let’s also dump the first eight elements of the AddressOfNames structure. Those elements can be seen on the picture below:
We can simply dump the appropriate names from the newly obtained RVAs with the da command that prints an ASCII string from the given virtual address until null bytes is detected.
The new table with both the function addresses and function names is presented below. Actually, we currently don’t know whether all of the addresses actually match the given name, because it’s the job of the AddressOfNameOrdinals to determine which function name belongs to which address. Later in the article, we’ll see that subsequent indexes are used, so the names and addresses in the table below match.
|Function Address||Function Name|
We can check very simply whether the addresses are right. We can do this with the u command. On the picture below, we can see three addresses, the first, the second, and the last, which match the table above.
After that, there’s only the AddressOfNameOrdinals array left to analyze. So far, we’ve mentioned that the first element of the AddressOfNameOrdinals should contain the index 0, because the first element in AddressOfNames array points to the first element in the AddressOfFunctions array. The second element in AddressOfNames points to the second element in the AddressOfFunctions, etc. To verify this, we can simply dump the AddressOfNameOrdinals array as can be seen below:
Notice that we used the dw command and not the dd command? This is because the values in the AddressOfNameOridinals are 16-bit numbers and not 32-bit numbers, which is why we must treat them like that.
In this article, we’ve presented the export directory structure as used by the PE header to export functions of the library. At first, we presented a theory on a simple example of 10 exported functions where 5 of them were exported by name and the other 5 were exported by original. After that, we presented a real example on kernel32.dll, where all functions were exported by name. We traversed all the arrays AddressOfFunctions, AddressOfNames and AddressOfNameOridinals and proven that the presented theory holds and is correct.