PE文件格式详解(6)

类别:VC语言 点击:0 评论:0 推荐:

导出数据段,.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是由决定偏移量地址和当前偏移量位置的方法来赋值的。偏移量的地址和偏移量本身都是相对虚拟地址,因此在使用之前必须进行转换——函数之中体现了这一点。虽然你可以编写一个类似的函数来决定顺序值或函数入口点,但是我为什么不为你做好呢?——GetNumberOfExportedFunctionsGetExportFunctionEntryPointsGetExportFunctionOrdinals已经存在于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,以及之后的GetDeviceCapsGetStockObjectGetTextMetrics等等。
  这样的式样会在.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是一个相对虚拟地址,它指向一个虚拟地址表。在文件装载的时候,这个虚拟地址表会被装载器置于段数据之中。但是在文件装载前,这些虚拟地址会被一些严密符合函数名称列表的虚拟地址替换。所以在文件装载之前,有两个同样的虚拟地址列表,它们指向导入函数列表。(未完待续)

本文地址:http://com.8s8s.com/it/it1259.htm