PE文件格式詳解(三)

導入數據段,.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是一個相對虛擬地址,它指向一個虛擬地址表。在文件裝載的時候,這個虛擬地址表會被裝載器置 於段數據之中。但是在文件裝載前,這些虛擬地址會被一些嚴密符合函數名稱列表的虛擬地址替換。所以在文件裝載之前,有兩個同樣的虛擬地址列表,它們指向導 入函數列表。

調試信息段,.debug

   調試信息位於.debug段之中,同時PE文件格式也支持單獨的調試文件(通常由.DBG擴展名標識)作爲一種將調試信息集中的方法。調試段包含了調試信 息,但是調試目錄卻位於早先提到的.rdata段之中。這其中每個目錄都涉及了.debug段之中的調試信息。調試目錄的結構 IMAGE_DEBUG_DIRECTORY被定義爲:
// WINNT.H

typedef struct _IMAGE_DEBUG_DIRECTORY {
  ULONG Characteristics;
  ULONG TimeDateStamp;
  USHORT MajorVersion;
  USHORT MinorVersion;
  ULONG Type;
  ULONG SizeOfData;
  ULONG AddressOfRawData;
  ULONG PointerToRawData;
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;
這個段被分爲單獨的部分,每個部分爲不同種類的調試信息數據。對於每個部分來說都是一個像上邊一樣的調試目錄。不同的調試信息種類如下:
// WINNT.H

#define IMAGE_DEBUG_TYPE_UNKNOWN 0
#define IMAGE_DEBUG_TYPE_COFF 1
#define IMAGE_DEBUG_TYPE_CODEVIEW 2
#define IMAGE_DEBUG_TYPE_FPO 3
#define IMAGE_DEBUG_TYPE_MISC 4
  
每 個目錄之中的Type域表示該目錄的調試信息種類。如你所見,在上邊的表中,PE文件格式支持很多不同的調試信息種類,以及一些其它的信息域。對於那些來 說,IMAGE_DEBUG_TYPE_MISC信息是唯一的。這一信息被添加到描述可執行映像的混雜信息之中,這些混雜信息不能被添加到PE文件格式任 何結構化的數據段之中。這就是映像文件中最合適的位置,映像名稱則肯定會出現在這裏。如果映像導出了信息,那麼導出數據段也會包含這一映像名稱。
    每種調試信息都擁有自己的頭部結構,該結構定義了它自己的數據。這些結構都列於WINNT.H之中。關於IMAGE_DEBUG_DIRECTORY一件 有趣的事就是它包括了兩個標識調試信息的域。第一個是AddressOfRawData,爲相對文件裝載的數據虛擬地址;另一個是 PointerToRawData,爲數據所在PE文件之中的實際偏移量。這就使得定位指定的調試信息相當容易了。
   作爲最後的例子,請你考慮以下的函數代碼,它從IMAGE_DEBUG_MISC結構中提取了映像名稱。
//PEFILE.C

int WINAPI RetrieveModuleName(LPVOID lpFile, HANDLE hHeap, char **pszModule)
{
  PIMAGE_DEBUG_DIRECTORY pdd;
  PIMAGE_DEBUG_MISC pdm = NULL;
  int nCnt;
  if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset(lpFile, 
      IMAGE_DIRECTORY_ENTRY_DEBUG)))
  return 0;
  while (pdd->SizeOfData)
  {
    if (pdd->Type == IMAGE_DEBUG_TYPE_MISC)
    {
      pdm = (PIMAGE_DEBUG_MISC)((DWORD)pdd->PointerToRawData + (DWORD)lpFile);
      nCnt = lstrlen(pdm->Data) * (pdm->Unicode ? 2 : 1);
      *pszModule = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt+1);
      CopyMemory(*pszModule, pdm->Data, nCnt);
      break;
    }
    pdd ++;
  }
  if (pdm != NULL)
    return nCnt;
  else
    return 0;
}
你看到了,調試目錄結構使得定位一個特定種類的調試信息變得相對容易了些。只要定位了IMAGE_DEBUG_MISC結構,提取映像名稱就如同調用CopyMemory函數一樣簡單。
   如上所述,調試信息可以被剝離到單獨的.DBG文件中。Windows NT SDK包含了一個名爲REBASE.EXE的程序可以實現這一目的。例如,以下的語句可以將一個名爲TEST.EXE的調試信息剝離:
   rebase -b 40000 -x c:/samples/testdir test.exe
    調試信息被置於一個新的文件中,這個文件名爲TEST.DBG,位於c:/samples/testdir之中。這個文件起始於一個單獨的 IMAGE_SEPARATE_DEBUG_HEADER結構,接着是存在於原可執行映像之中的段頭部的一份拷貝。在段頭部之後,是.debug段的數 據。也就是說,在段頭部之後,就是一系列的IMAGE_DEBUG_DIRECTORY結構及其相關的數據了。調試信息本身保留了如上所描述的常規映像文 件調試信息。

PE文件格式總結

   Windows NT的PE文件格式向熟悉Windows和MS-DOS環境的開發者引入了一種全新的結構。然而熟悉UNIX環境的開發者會發現PE文件格式與COFF規範很相像(如果它不是以COFF爲基礎的話)。
   整個格式的組成:一個MS-DOS的MZ頭部,之後是一個實模式的殘餘程序、PE文件標誌、PE文件頭部、PE可選頭部、所有的段頭部,最後是所有的段實體。
   可選頭部的末尾是一個數據目錄入口的數組,這些相對虛擬地址指向段實體之中的數據目錄。每個數據目錄都表示了一個特定的段實體數據是如何組織的。
   PE文件格式有11個預定義段,這是對Windows NT應用程序所通用的,但是每個應用程序可以爲它自己的代碼以及數據定義它自己獨特的段。
   .debug預定義段也可以分離爲一個單獨的調試文件。如果這樣的話,就會有一個特定的調試頭部來用於解析這個調試文件,PE文件中也會有一個標誌來表示調試數據被分離了出去。

PEFILE.DLL函數描述

    PEFILE.DLL主要由一些函數組成,這些函數或者被用來獲得一個給定的PE文件中的偏移量,或者被用來把文件中的一些數據複製到一個特定的結構中 去。每個函數都有一個需求——第一個參數是一個指針,這個指針指向PE文件的起始處。也就是說,這個文件必須首先被映射到你進程的地址空間中,然後映射文 件的位置就可以作爲每個函數第一個參數的lpFile的值來傳入了。
   我意在使函數的名稱使你能夠一見而知其意,並且每個函數都隨一個詳細描述其目的的註釋而列出。如果在讀完函數列表之後,你仍然不明白某個函數的功能,那麼 請參考EXEVIEW.EXE示例來查明這個函數是如何使用的。以下的函數原型列表可以在PEFILE.H中找到:
// PEFILE.H

/* 獲得指向MS-DOS MZ頭部的指針 */
BOOL WINAPI GetDosHeader(LPVOID, PIMAGE_DOS_HEADER);

/* 決定.EXE文件的類型 */
DWORD WINAPI ImageFileType(LPVOID);

/* 獲得指向PE文件頭部的指針 */
BOOL WINAPI GetPEFileHeader(LPVOID, PIMAGE_FILE_HEADER);

/* 獲得指向PE可選頭部的指針 */
BOOL WINAPI GetPEOptionalHeader(LPVOID, PIMAGE_OPTIONAL_HEADER);

/* 返回模塊入口點的地址 */
LPVOID WINAPI GetModuleEntryPoint(LPVOID);

/* 返回文件中段的總數 */
int WINAPI NumOfSections(LPVOID);

/* 返回當可執行文件被裝載入進程地址空間時的首選基地址 */
LPVOID WINAPI GetImageBase(LPVOID);

/* 決定文件中一個特定的映像數據目錄的位置 */
LPVOID WINAPI ImageDirectoryOffset(LPVOID, DWORD);

/* 獲得文件中所有段的名稱 */
int WINAPI GetSectionNames(LPVOID, HANDLE, char **);

/* 複製一個特定段的頭部信息 */
BOOL WINAPI GetSectionHdrByName(LPVOID, PIMAGE_SECTION_HEADER, char *);

/* 獲得由空字符分隔的導入模塊名稱列表 */
int WINAPI GetImportModuleNames(LPVOID, HANDLE, char **);

/* 獲得一個模塊由空字符分隔的導入函數列表 */
int WINAPI GetImportFunctionNamesByModule(LPVOID, HANDLE, char *, char **);

/* 獲得由空字符分隔的導出函數列表 */
int WINAPI GetExportFunctionNames(LPVOID, HANDLE, char **);

/* 獲得導出函數總數 */
int WINAPI GetNumberOfExportedFunctions(LPVOID);

/* 獲得導出函數的虛擬地址入口點列表 */
LPVOID WINAPI GetExportFunctionEntryPoints(LPVOID);

/* 獲得導出函數順序值列表 */
LPVOID WINAPI GetExportFunctionOrdinals(LPVOID);

/* 決定資源對象的種類 */
int WINAPI GetNumberOfResources (LPVOID);

/* 返回文件中所使用的所有資源對象的種類 */
int WINAPI GetListOfResourceTypes(LPVOID, HANDLE, char **);

/* 決定調試信息是否已從文件中分離 */
BOOL WINAPI IsDebugInfoStripped(LPVOID);

/* 獲得映像文件名稱 */
int WINAPI RetrieveModuleName(LPVOID, HANDLE, char **);

/* 決定文件是否是一個有效的調試文件 */
BOOL WINAPI IsDebugFile(LPVOID);

/* 從調試文件中返回調試頭部 */
BOOL WINAPI GetSeparateDebugHeader(LPVOID, PIMAGE_SEPARATE_DEBUG_HEADER);
  除了以上所列的函數之外,本文中早先提到的宏也定義在了PEFILE.H中,完整的列表如下:
/* PE文件標誌的偏移量 */
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + /
                       ((PIMAGE_DOS_HEADER)a)->e_lfanew))

/* MS操作系統頭部標識了雙字的NT PE文件標誌;PE文件頭部就緊跟在這個雙字之後 */
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + /
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + /
                        SIZE_OF_NT_SIGNATURE))

/* PE可選頭部緊跟在PE文件頭部之後 */
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + /
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + /
                        SIZE_OF_NT_SIGNATURE + /
                        sizeof(IMAGE_FILE_HEADER)))

/* 段頭部緊跟在PE可選頭部之後 */
#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a + /
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + /
                        SIZE_OF_NT_SIGNATURE + /
                        sizeof(IMAGE_FILE_HEADER) + /
                        sizeof(IMAGE_OPTIONAL_HEADER)))
  
要 使用PEFILE.DLL,你只用包含PEFILE.H文件並在應用程序中鏈接到這個DLL即可。所有的這些函數都是互斥性的函數,但是有些函數的功能可 以相互支持以獲得文件信息。例如,GetSectionNames可以用於獲得所有段的名稱,這樣一來,爲了獲得一個擁有獨特段名稱(在編譯期由應用程序 開發者定義的)的段頭部,你就需要首先獲得所有名稱的列表,然後再對那個準確的段名稱調用函數GetSectionHeaderByName了。現在,你 可以享受我爲你帶來的這一切了!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章