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.



Theory:

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.

Field Name Meaning
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.

AddressOfNames AddressOfNameOrdinals
|
|
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

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:

  1. Go to the PE header
  2. Read the virtual address of the export table in the data directory
  3. Go to the export table and obtain the number of names (NumberOfNames)
  4. 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.
  5. 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.

With the discussion of nBase out of the way, we can continue to the next example.
Suppose that we have an ordinal of a function and we need to obtain the address of that function, we can do it like this:
  1. Go to the PE header
  2. Obtain the RVA of the export table from the data directory
  3. Go to the export table and obtain the value of nBase.
  4. Subtract the ordinal by the value in nBase and you have the index into the AddressOfFunctions array.
  5. Compare the index with the value in NumberOfFunctions. If the index is larger or equal to the value in NumberOfFunctions, the ordinal is invalid.
  6. 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.

If a function is exported by name, you can use either its name or its ordinal in GetProcAddress. But what if the function is exported by ordinal only? We come to that now.
"A function is exported by ordinal only" means the function doesn't have entries in both AddressOfNames and AddressOfNameOrdinals arrays. Remember the two fields, NumberOfFunctions and NumberOfNames. The existence of these two fields is the evidence that some functions may not have names. The number of functions must be at least equal to the number of names. The functions that don't have names are exported by their ordinals only. For example, if there are 70 functions but only 40 entries in the AddressOfNames array, it means there are 30 functions in the module that are exported by their ordinals only. Now how can we find out which functions are exported by ordinals only? It's not easy. You must find that out by exclusion, ie. the entries in the AddressOfFunctions array that are not referenced by the AddressOfNameOrdinals array contain the RVAs of the functions that are exported by ordinals only.

Example:

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.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD


SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
        invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
          mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
    .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi<eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
    invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

Analysis:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

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.

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

We display the important information in the IMAGE_EXPORT_DIRECTORY structure in the edit control.

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

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.

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

The addresses of the three arrays are stored in esi, ebx, and edi, ready to be accessed.

.while NumberOfNames>0

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.

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

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.

   shl edx,2
   add edx,edi

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.

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

We display the RVA, ordinal, and the name of the function in the edit control.

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

Update the counter and the addresses of the current elements in AddressOfNames and AddressOfNameOrdinals arrays. Continue until all names are processed.