PE檔案格式詳解(六)
匯出資料段,.edata
.edata段包含了應用程式或DLL的匯出資料。在這個段出現的時候,它會包含一個到達匯出資訊的匯出目錄。
WINNT.H
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Name;
ULONG Base;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
PULONG *AddressOfFunctions;
PULONG *AddressOfNames;
PUSHORT *AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
匯出目錄中的Name域標識了可執行模組的名稱。NumberOfFunctions域和NumberOfNames域表示模組中有多少匯出的函式以及這些函式的名稱。
AddressOfFunctions域是一個到匯出函式入口列表的偏移量。AddressOfNames域是到一個匯出函式名稱列表起始處偏移量的地址,這個列表是由null分隔的。AddressOfNameOrdinals是一個到相同匯出函式順序值(每個值2位元組長)列表的偏移量。
三個AddressOf...域是當模組裝載時程序地址空間中的相對虛擬地址。一旦模組被裝載,那麼要獲得程序地質空間中的確切地址的話,就應該在相對虛擬地址上加上模組的基地址。可是,在檔案被裝載前,仍然可以決定這一地址:只要從給定的域地址中減去段頭部的虛擬地址(VirtualAddress),再加上段實體的偏移量(PointerToRawData),這個結果就是映像檔案中的偏移量了。以下的例子解說了這一技術:
PEFILE.C
int WINAPI GetExportFunctionNames(LPVOID lpFile, HANDLE hHeap,
char **pszFunctions)
{
IMAGE_SECTION_HEADER sh;
PIMAGE_EXPORT_DIRECTORY ped;
char *pNames, *pCnt;
int i, nCnt;
/* 獲得.edata域中的段頭部和指向資料目錄的指標 */
if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset
(lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)
return 0;
GetSectionHdrByName(lpFile, &sh, ".edata");
/* 決定匯出函式名稱的偏移量 */
pNames = (char *)(*(int *)((int)ped->AddressOfNames -
(int)sh.VirtualAddress + (int)sh.PointerToRawData +
(int)lpFile) - (int)sh.VirtualAddress +
(int)sh.PointerToRawData + (int)lpFile);
/* 計算出要為所有的字串分配多少記憶體 */
pCnt = pNames;
for (i = 0; i < (int)ped->NumberOfNames; i++)
while (*pCnt++);
nCnt = (int)(pCnt.pNames);
/* 在堆上為函式名稱分配記憶體 */
*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);
/* 將所有字串複製到緩衝區 */
CopyMemory ((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);
return nCnt;
}
請注意,在這個函式之中,變數pNames是由決定偏移量地址和當前偏移量位置的方法來賦值的。偏移量的地址和偏移量本身都是相對虛擬地址,因此在使用之前必須進行轉換——函式之中體現了這一點。雖然你可以編寫一個類似的函式來決定順序值或函式入口點,但是我為什麼不為你做好呢?——GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和GetExportFunctionOrdinals已經存在於PEFILE.DLL之中了。
匯入資料段,.idata
.idata段是匯入資料,包括匯入庫和匯入地址名稱表。雖然定義了IMAGE_DIRECTORY_ENTRY_IMPORT,但是WINNT.H之中並無相應的匯入目錄結構。作為代替,其中有若干其它的結構,名為IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA與IMAGE_IMPORT_DESCRIPTOR。在我個人看來,我實在不知道這些結構是如何和.idata段發生關聯的,所以我花了若干個小時來破譯.idata段實體並且得到了一個更簡單的結構,我名之為IMAGE_IMPORT_MODULE_DIRECTORY。
PEFILE.H
typedef struct tagImportDirectory
{
DWORD dwRVAFunctionNameList;
DWORD dwUseless1;
DWORD dwUseless2;
DWORD dwRVAModuleName;
DWORD dwRVAFunctionAddressList;
} IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;
和其它段的資料目錄不同的是,這個是作為檔案中的每個匯入模組重複出現的。你可以將它看作模組資料目錄列表中的一個入口,而不是一個整個資料段的資料目錄。每個入口都是一個指向特定模組匯入資訊的目錄。
IMAGE_IMPORT_MODULE_DIRECTORY結構中的一個域dwRVAModuleName是一個相對虛擬地址,它指向模組的名稱。結構中還有兩個dwUseless引數,它們是為了保持段的對齊。PE檔案格式規範提到了一些東西,關於匯入標記、時間/日期標誌以及主/次版本,但是在我的實驗中,這兩個域自始而終都是空的,所以我仍然認為它們沒有什麼用處。
基於這個結構的定義,你便可以獲得可執行檔案中匯入的所有模組和函式名稱了。以下的函式示範瞭如何獲得特定的PE檔案中的所有匯入函式名稱:
PEFILE.C
int WINAPI GetImportModuleNames(LPVOID lpFile, HANDLE hHeap,
char **pszModules)
{
PIMAGE_IMPORT_MODULE_DIRECTORY pid;
IMAGE_SECTION_HEADER idsh;
BYTE *pData;
int nCnt = 0, nSize = 0, i;
char *pModule[1024];
char *psz;
pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset
(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
pData = (BYTE *)pid;
/* 定位.idata段頭部 */
if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))
return 0;
/* 提取所有匯入模組 */
while (pid->dwRVAModuleName)
{
/* 為絕對字串偏移量分配緩衝區 */
pModule[nCnt] = (char *)(pData +
(pid->dwRVAModuleName-idsh.VirtualAddress));
nSize += strlen(pModule[nCnt]) + 1;
/* 增至下一個匯入目錄入口 */
pid++;
nCnt++;
}
/* 將所有字串賦值到一大塊的堆記憶體中 */
*pszModules = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nSize);
psz = *pszModules;
for (i = 0; i < nCnt; i++)
{
strcpy(psz, pModule[i]);
psz += strlen (psz) + 1;
}
return nCnt;
}
這個函式非常好懂,然而有一點值得指出——注意while迴圈。這個迴圈當pid->dwRVAModuleName為0的時候終止,這就暗示了在IMAGE_IMPORT_MODULE_DIRECTORY結構列表的末尾有一個空的結構,這個結構擁有一個0值,至少dwRVAModuleName域為0。這便是我在對檔案的實驗中以及之後在PE檔案格式中研究的行為。
這個結構中的第一個域dwRVAFunctionNameList是一個相對虛擬地址,這個地址指向一個相對虛擬地址的列表,這些地址是檔案中的一些檔名。如下面的資料所示,所有匯入模組的模組和函式名稱都列於.idata段資料中了:
E6A7 0000 F6A7 0000 08A8 0000 1AA8 0000 ................
28A8 0000 3CA8 0000 4CA8 0000 0000 0000 (...<...L.......
0000 4765 744F 7065 6E46 696C 654E 616D ..GetOpenFileNam
6541 0000 636F 6D64 6C67 3332 2E64 6C6C eA..comdlg32.dll
0000 2500 4372 6561 7465 466F 6E74 496E ..%.CreateFontIn
6469 7265 6374 4100 4744 4933 322E 646C directA.GDI32.dl
6C00 A000 4765 7444 6576 6963 6543 6170 l...GetDeviceCap
7300 C600 4765 7453 746F 636B 4F62 6A65 s...GetStockObje
6374 0000 D500 4765 7454 6578 744D 6574 ct....GetTextMet
7269 6373 4100 1001 5365 6C65 6374 4F62 ricsA...SelectOb
6A65 6374 0000 1601 5365 7442 6B43 6F6C ject....SetBkCol
6F72 0000 3501 5365 7454 6578 7443 6F6C or..5.SetTextCol
6F72 0000 4501 5465 7874 4F75 7441 0000 or..E.TextOutA..
以上的資料是EXEVIEW.EXE示例程式.idata段的一部分。這個特別的段表示了匯入模組列表和函式名稱列表的起始處。如果你開始檢查資料中的這個段,你應該認出一些熟悉的Win32 API函式以及模組名稱。從上往下讀的話,你可以找到GetOpenFileNameA,緊接著是COMDLG32.DLL。然後你能發現CreateFontIndirectA,緊接著是模組GDI32.DLL,以及之後的GetDeviceCaps、GetStockObject、GetTextMetrics等等。
這樣的式樣會在.idata段中重複出現。第一個模組是COMDLG32.DLL,第二個是GDI32.DLL。請注意第一個模組只匯出了一個函式,而第二個模組匯出了很多函式。在這兩種情況下,函式和模組的排列的方法是首先出現一個函式名,之後是模組名,然後是其它的函式名(如果有的話)。
以下的函式示範瞭如何獲得指定模組的所有函式名。
PEFILE.C
int WINAPI GetImportFunctionNamesByModule(LPVOID lpFile,
HANDLE hHeap, char *pszModule, char **pszFunctions)
{
PIMAGE_IMPORT_MODULE_DIRECTORY pid;
IMAGE_SECTION_HEADER idsh;
DWORD dwBase;
int nCnt = 0, nSize = 0;
DWORD dwFunction;
char *psz;
/* 定位.idata段的頭部 */
if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))
return 0;
pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset
(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
dwBase = ((DWORD)pid. idsh.VirtualAddress);
/* 查詢模組的pid */
while (pid->dwRVAModuleName && strcmp (pszModule,
(char *)(pid->dwRVAModuleName+dwBase)))
pid++;
/* 如果模組未找到,就退出 */
if (!pid->dwRVAModuleName)
return 0;
/* 函式的總數和字串長度 */
dwFunction = pid->dwRVAFunctionNameList;
while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&
*(char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2))
{
nSize += strlen ((char *)((*(DWORD *)(dwFunction + dwBase))
+ dwBase+2)) + 1;
dwFunction += 4;
nCnt++;
}
/* 在堆上分配函式名稱的空間 */
*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
psz = *pszFunctions;
/* 向記憶體指標複製函式名稱 */
dwFunction = pid->dwRVAFunctionNameList;
while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&
*((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)))
{
strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) +
dwBase+2));
psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+
dwBase+2)) + 1;
dwFunction += 4;
}
return nCnt;
}
就像GetImportModuleNames函式一樣,這一函式依靠每個資訊列表的末端來獲得一個置零的入口。這在種情況下,函式名稱列表就是以零結尾的。
最後一個域dwRVAFunctionAddressList是一個相對虛擬地址,它指向一個虛擬地址表。在檔案裝載的時候,這個虛擬地址表會被裝載器置於段資料之中。但是在檔案裝載前,這些虛擬地址會被一些嚴密符合函式名稱列表的虛擬地址替換。所以在檔案裝載之前,有兩個同樣的虛擬地址列表,它們指向匯入函式列表。(未完待續)