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.
Download the
example.
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:
- 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.
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:
- 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.
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.
[Iczelion's
Win32 Assembly Homepage]
|