Tutorial 7: Export Table
We have learned about one part of the dynamic linking, namely the import table, in the previous tutorial. Now we will learn about the other side of the coin, the export table.
When the PE loader runs a program, it loads the associated DLLs into the process address space. It then extracts information about the import functions from the main program. It uses the information to search the DLLs for the addresses of the functions to be patched into the main program. The place in the DLLs where the PE loader looks for the addresses of the functions is the export table.
When a DLL/EXE exports a function to be used by other DLL/EXE, it can do so in two ways: it can export the function by name or by ordinal only. Say if there is a function named "GetSysConfig" in a DLL, it can choose to tell the other DLLs/EXEs that if they want to call the function, they must specify it by its name, ie. GetSysConfig. The other way is to export by ordinal. What's an ordinal? An ordinal is a 16-bit number that uniquely identifies a function in a particular DLL. This number is unique only within the DLL it refers to. For example, in the above example, the DLL can choose to export the function by ordinal, say, 16. Then the other DLLs/EXEs which want to call this function must specify this number in GetProcAddress. This is called export by ordinal only.
Export by ordinal only is strongly discouraged because it can cause a maintenance problem for the DLL. If the DLL is upgraded/updated, the programmer of that DLL cannot alter the ordinals of the functions else other programs that depend on the DLL will break.
Now we can examine the export structure. As with import table, you can find where the export table is from looking at the data directory. In this case, the export table is the first member of the data directory. The export structure is called IMAGE_EXPORT_DIRECTORY. There are 11 members in the structure but only some of them are really used.
|nName||The actual name of the module. This field is necessary because the name of the file can be changed. If it's the case, the PE loader will use this internal name.|
|nBase||A number that you must bias against the ordinals to get the indexes into the address-of-function array.|
|NumberOfFunctions||Total number of functions/symbols that are exported by this module.|
|NumberOfNames||Number of functions/symbols that are exported by name. This value is not the number of ALL functions/symbols in the module. For that number, you need to check NumberOfFunctions. This value can be 0. In that case, the module may export by ordinal only. If there is no function/symbol to be exported in the first case, the RVA of the export table in the data directory will be 0.|
|AddressOfFunctions||An RVA that points to an array of RVAs of the functions/symbols in the module. In short, RVAs to all functions in the module are kept in an array and this field points to the head of that array.|
|AddressOfNames||An RVA that points to an array of RVAs of the names of functions in the module.|
|AddressOfNameOrdinals||An RVA that points to a 16-bit array that contains the ordinals associated with the function names in the AddressOfNames array above.|
Just reading the above table may not give you the real picture of the export table. The simplified explanation below will clarify the concept.
The export table exists for use by the PE loader. First of all, the module must keep the addresses of all exported functions somewhere so the PE loader can look them up. It keeps them in an array that is pointed to by the field AddressOfFunctions. The number of elements in the array is kept in NumberOfFunctions. Thus if the module exports 40 functions, it must have 40 members in the array pointed to by AddressOfFunctions and NumberOfFunctions must contain a value 40. Now if some functions are exported by names, the module must keep the names in the file. It keeps the RVAs to the names in an array so the PE loader can look them up. That array is pointed to by AddressOfNames and the number of names in NumberOfNames. Think about the job of the PE loader, it knows the names of the functions, it must somehow obtain the addresses of those functions. Up to now, the module has two arrays: the names and the addresses but there is no linkage between them. Thus we need something that relates the names of the functions to their addresses. The PE specification uses indexes into the address array as that essential linkage. Thus if the PE loader finds the name it looks for in the name array, it can obtain the index into the address table for that name too. The indexes are kept in another array (the last one) pointed to by the field AddressOfNameOrdinals. Since this array exists as the linkage between the names and the addresses, it must have exactly the same number of elements as the name array, ie. each name can have one and only one associated address. The reverse is not true: an address may have several names associated with it. Thus we can have "aliases" that refer to the same address. To make the linkage works, both name and index arrays must run in parallel, ie. the first element in the index array must hold the index for the first name and so on.
An example or two is in order. If we have the name of an export function and we need to get its address in the module, we can do like this:
- Go to the PE header
- Read the virtual address of the export table in the data directory
- Go to the export table and obtain the number of names (NumberOfNames)
- Walk the arrays pointed to by AddressOfNames and AddressOfNameOrdinals in parallel, searching for the matching name. If the name is found in the AddressOfNames array, you must extract the value in the associated element in the AddressOfNameOrdinals array. For example, if you find the RVA of the matching name in 77th element of the AddressOfNames array, you must extract the value stored in the 77th element of the AddressOfNameOrdinals array. If you walk the array until NumberOfNames elements are examined, you know that the name is not in this module.
- Use the value from the AddressOfNameOrdinals array as the index into the AddressOfFunctions array. Say, if the value is 5, you must extract the value in the 5th element of the AddressOfFunctions array. That value is the RVA of the function.
Now we can turn our attention to the nBase member of the IMAGE_EXPORT_DIRECTORY structure. You already know that the AddressOfFunctions array contains the addresses of all export symbols in a module. And the PE loader uses the indexes into this array to find the addresses of the functions. Let's imagine the scenario where we use the indexes into this array as the ordinals. Since the programmers can specify the starting ordinal number in .def file, like 200, it means that there must be at least 200 elements in the AddressOfFunctions array. Furthermore the first 200 elements are not used but they must exist so that the PE loader can use the indexes to find the correct addresses. This is not good at all. The nBase member exists to solve this problem. If the programmer specifies the starting ordinal of 200, the value in nBase would be 200. When the PE loader reads the value in nBase, it knows that the first 200 elements do not exist and that it should subtract the ordinal by the value in nBase to obtain the true index into the AddressOfFunctions array. With the use of nBase, there is no need to provide 200 empty elements.
Note that nBase doesn't affect the values in the AddressOfNameOrdinals array. Despite the name "AddressOfNameOrdinals", this array contains the true indexes into the AddressOfFunctions array, not the ordinals.
- Go to the PE header
- Obtain the RVA of the export table from the data directory
- Go to the export table and obtain the value of nBase.
- Subtract the ordinal by the value in nBase and you have the index into the AddressOfFunctions array.
- Compare the index with the value in NumberOfFunctions. If the index is larger or equal to the value in NumberOfFunctions, the ordinal is invalid.
- Use the index to obtain the RVA of the function in the AddressOfFunctions array.
Note that obtaining the address of a function from an ordinal is much easier and faster than using the name of the function. There is no need to walk the AddressOfNames and AddressOfNameOrdinals arrays. The performance gain, however, must be balanced against the difficulty in the maintaining the module.
In conclusion, if you want to obtain the address of a function from its name, you need to walk both AddressOfNames and AddressOfNameOrdinals arrays to obtain the index into the AddressOfFunctions array. If you have the ordinal of the function, you can go directly to the AddressOfFunctions array after the ordinal is biased by nBase.
This example is similar to the one in the previous tutorial. However, it displays the values of some members of IMAGE_EXPORT_DIRECTORY structure and also lists the RVAs, ordinals, and names of the exported functions. Note that this example doesn't list the functions that are exported by ordinals only.
After the program verifies that the file is a valid PE, it goes to the data directory and obtains the virtual address of the export table. If the virtual address is zero, the file doesn't have any exported symbol.
We display the important information in the IMAGE_EXPORT_DIRECTORY structure in the edit control.
Since we want to enumerate all function names, we need to know how many names there are in the export table. nBase is used when we want to convert the indexes into the AddressOfFunctions array into ordinals.
The addresses of the three arrays are stored in esi, ebx, and edi, ready to be accessed.
Continue until all names are processed.
invoke RVAToFileMap,pMapping,dword ptr [esi]
Since esi points to an array of RVAs of the exported names, dereference it will give the RVA of the current name. We convert it to the virtual address, to be used in wsprintf later.
ebx points to the array of ordinals. Its array elements are word-size. Thus we need to convert the value into a dword first. edx and ecx contain the index into the AddressOfFunctions array. We will use edx as the pointer into the AddressOfFunctions array. We add the value of nBase to ecx to obtain the ordinal number of the function.
We multiply the index by 4 (each element in the AddressOfFunctions array is 4 bytes in size) and then add the address of the AddressOfFunctions array to it. Thus edx points to the RVA of the function.
We display the RVA, ordinal, and the name of the function in the edit control.
Update the counter and the addresses of the current elements in AddressOfNames and AddressOfNameOrdinals arrays. Continue until all names are processed.