iczelion pe tutcn7

 

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. 並行遍歷AddressOfNamesAddressOfNameOrdinals指向的數組匹配名字。如果在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了。

可以看出,從序數獲取函數地址比函數名快捷容易。不需要遍歷AddressOfNamesAddressOfNameOrdinals 這兩個數組。然而,綜合性能必須與模塊維護的簡易程度作一平衡。

總之,如果想通過名字獲取函數地址,需要遍歷AddressOfNamesAddressOfNameOrdinals 這兩個數組。如果使用函數序數,減掉nBase值後就可直接索引AddressOfFunctions 數組。

如果一函數通過名字引出,那在GetProcAddress中可以使用名字或序數。但函數僅由序數引出情況又怎樣呢? 現在就來看看。
"一個函數僅由序數引出"意味着函數在
AddressOfNames AddressOfNameOrdinals 數組中不存在相關項。記住兩個域,NumberOfFunctionsNumberOfNames。這兩個域可以清楚地顯示有時某些函數沒有名字的。函數數目至少等同於名字數目,沒有名字的函數通過序數引出。比如,如果存在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 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

分析:

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

修正計數器,AddressOfNamesAddressOfNameOrdinals 兩數組的當前指針,繼續遍歷直到所有名字全都處理完畢。


翻譯:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章