ITEEDU

PE教程7: Export Table(引出表)

上一课我们已经学习了动态联接中关于引入表那部分知识,现在继续另外一部分,那就是引出表。

 

理论:

当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的引出函数。

DLL/EXE要引出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名引出或者仅仅通过序数引出。比如某个DLL要引出名为"GetSysConfig"的函数,如果它以函数名引出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是GetSysConfig。另外一个办法就是通过序数引出。什么是序数呢? 序数是唯一指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数引出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。

我们不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级/修改,程序员无法改变函数的序数,否则调用该DLL的其他程序都将无法工作。

现在我们开始学习引出结构。象引出表一样,可以通过数据目录找到引出表的位置。这儿,引出表是数据目录的第一个成员,又可称为IMAGE_EXPORT_DIRECTORY。该结构中共有11 个成员,常用的列于下表。

Field Name Meaning
nName 模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。
nBase 基数,加上序数就是函数地址数组的索引值了。
NumberOfFunctions 模块引出的函数/符号总数。
NumberOfNames 通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中引出表的RVA为0。
AddressOfFunctions 模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。
AddressOfNames 类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。
AddressOfNameOrdinals RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。

上面也许无法让您完全理解引出表,下面的简述将助您一臂之力。

引出表的设计是为了方便PE装载器工作。首先,模块必须保存所有引出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。 因此,如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。现在如果有一些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些 名字的RVAs存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个模块: 名字数组和地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取了 指向地址表中对应元素的索引。 而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,以此类推。

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

下面举一两个例子说明问题。如果我们有了引出函数名并想以此获取地址,可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取名字数目(NumberOfNames)。
  4. 并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字。如果在AddressOfNames 指向的数组中找到匹配名字,从AddressOfNameOrdinals 指向的数组中提取索引值。例如,若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素,那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完NumberOfNames 个元素,说明当前模块没有所要的名字。
  5. 从AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也就是说,如果值是5,就必须读取AddressOfFunctions 数组的第5个元素,此值就是所要函数的RVA。

现在我们在把注意力转向IMAGE_EXPORT_DIRECTORY 结构的nBase成员。您已经知道AddressOfFunctions 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时,让我们设想这样一种情况,如果程序员在.def文件中设定起始序数号为200,这意味着AddressOfFunctions 数组至少有200个元素,甚至这前面200个元素并没使用,但它们必须存在,因为PE装载器这样才能索引到正确的地址。这种方法很不好,所以又设计了nBase 域解决这个问题。如果程序员指定起始序数号为200,nBase 值也就是200。当PE装载器读取nBase域时,它知道开始200个元素并不存在,这样减掉一个nBase值后就可以正确地索引AddressOfFunctions 数组了。有了nBase,就节约了200个空元素。

注意nBase并不影响AddressOfNameOrdinals数组的值。尽管取名"AddressOfNameOrdinals",该数组实际包含的是指向AddressOfFunctions 数组的索引,而不是什么序数啦。

讨论完nBase的作用,我们继续下一个例子。
假设我们只有函数的序数,那么怎样获取函数地址呢,可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取nBase值。
  4. 减掉nBase值得到指向AddressOfFunctions 数组的索引。
  5. 将该值与NumberOfFunctions作比较,大于等于后者则序数无效。
  6. 通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。

可以看出,从序数获取函数地址比函数名快捷容易。不需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。然而,综合性能必须与模块维护的简易程度作一平衡。

总之,如果想通过名字获取函数地址,需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。如果使用函数序数,减掉nBase值后就可直接索引AddressOfFunctions 数组。

如果一函数通过名字引出,那在GetProcAddress中可以使用名字或序数。但函数仅由序数引出情况又怎样呢? 现在就来看看。
"一个函数仅由序数引出"意味着函数在AddressOfNames 和 AddressOfNameOrdinals 数组中不存在相关项。记住两个域,NumberOfFunctions 和 NumberOfNames。这两个域可以清楚地显示有时某些函数没有名字的。函数数目至少等同于名字数目,没有名字的函数通过序数引出。比如,如果存在70个函数但AddressOfNames数组中只有40项,这就意味着模块中有30个函数是仅通过序数引出的。现在我们怎样找出那些仅通过序数引出的函数呢?这不容易,必须通过排除法,比如,AddressOfFunctions 的数组项在AddressOfNameOrdinals 数组中不存在相关指向,这就说明该函数RVA只通过序数引出。

示例:

本例类似上课的范例。然而,在显示IMAGE_EXPORT_DIRECTORY 结构一些成员信息的同时,也列出了引出函数的RVAs,序数和名字。注意本例没有列出仅由序数引出的函数。

.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      uses edx 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           edi0
              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

分析:

              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

程序检验PE有效性后,定位到数据目录获取引出表的虚拟地址。若该虚拟地址为0,则文件不含引出符号。

              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

在编辑控件中显示IMAGE_EXPORT_DIRECTORY 结构的一些重要信息。

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

由于我们要枚举所有函数名,就要知道引出表里的名字数目。nBase 在将AddressOfFunctions 数组索引转换成序数时派到用场。

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

将三个数组的地址相应存放到esi,,ebx,edi中。准备开始访问。

.while NumberOfNames>0 

直到所有名字都被处理完毕。

   invoke
RVAToFileMap,pMapping,dword ptr [esi] 

由于esi指向包含名字字符串RVAs的数组,所以[esi]含有当前名字的RVA,需要将它转换成虚拟地址,后面wsprintf要用的。

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

ebx指向序数数组,值是字类型的。因此我们先要将其转换成双字,此时edx和ecx含有指向AddressOfFunctions 数组的索引。我们用edx作为索引值,而将ecx加上nBase得到函数的序数值。=

   shl edx,2 
add edx,edi

索引乘以4 (AddressOfFunctions 数组中每个元素都是4字节大小) 然后加上数组首地址,这样edx指向的就是所要函数的RVA了。

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

在编辑控件中显示函数的RVA, 序数, 和名字。

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

修正计数器,AddressOfNames 和 AddressOfNameOrdinals 两数组的当前指针,继续遍历直到所有名字全都处理完毕。