热度 37|
PE文件格式详解(下)
作者:MSDN
译者:李马
预定义段
一个Windows
NT的应用程序典型地拥有9个预定义段,它们是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些应用程序不需要所有的这些段,同样还有一些应用程序为了自己特殊的需要而定义了更多的段。这种做法与MS-DOS和Windows
3.1中的代码段和数据段相似。事实上,应用程序定义一个独特的段的方法是使用标准编译器来指示对代码段和数据段的命名,或者使用名称段编译器选项-NT——就和Windows
3.1中应用程序定义独特的代码段和数据段一样。
以下是一个关于Windows NT PE文件之中一些有趣的公共段的讨论。
可执行代码段,.text
Windows 3.1和Windows
NT之间的一个区别就是Windows NT默认的做法是将所有的代码段(正如它们在Windows
3.1中所提到的那样)组成了一个单独的段,名为“.text”。既然Windows
NT使用了基于页面的虚拟内存管理系统,那么将分开的代码放入不同的段之中的做法就不太明智了。因此,拥有一个大的代码段对于操作系统和应用程序开发者来说,都是十分方便的。
.text段也包含了早先提到过的入口点。IAT亦存在于.text段之中的模块入口点之前。(IAT在.text段之中的存在非常有意义,因为这个表事实上是一系列的跳转指令,并且它们的跳转目标位置是已固定的地址。)当Windows
NT的可执行映像装载入进程的地址空间时,IAT就和每一个导入函数的物理地址一同确定了。要在.text段之中查找IAT,装载器只用将模块的入口点定位,而IAT恰恰出现于入口点之前。既然每个入口拥有相同的尺寸,那么向后退查找这个表的起始位置就很容易了。
数据段,.bss、.rdata、.data
.bss段表示应用程序的未初始化数据,包括所有函数或源模块中声明为static的变量。
.rdata段表示只读的数据,比如字符串文字量、常量和调试目录信息。
所有其它变量(除了出现在栈上的自动变量)存储在.data段之中。基本上,这些是应用程序或模块的全局变量。
资源段,.rsrc
.rsrc段包含了模块的资源信息。它起始于一个资源目录结构,这个结构就像其它大多数结构一样,但是它的数据被更进一步地组织在了一棵资源树之中。以下的IMAGE_RESOURCE_DIRECTORY结构形成了这棵树的根和各个结点。
//WINNT.H typedef struct _IMAGE_RESOURCE_DIRECTORY { ULONG Characteristics; ULONG TimeDateStamp; USHORT MajorVersion; USHORT MinorVersion; USHORT NumberOfNamedEntries; USHORT NumberOfIdEntries; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;请看这个目录结构,你将会发现其中竟然没有指向下一个结点的指针。但是,在这个结构中有两个域NumberOfNamedEntries和NumberOfIdEntries代替了指针,它们被用来表示这个目录附有多少入口。附带说一句,我的意思是目录入口就在段数据之中的目录后边。有名称的入口按字母升序出现,再往后是按数值升序排列的ID入口。
// WINNT.H typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { ULONG Name; ULONG OffsetToData; } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;根据树的层级不同,这两个域也就有着不同的用途。Name域被用于标识一个资源种类,或者一种资源名称,或者一个资源的语言ID。OffsetToData与常常被用来在树之中指向兄弟结点——即一个目录结点或一个叶子结点。
// WINNT.H typedef struct _IMAGE_RESOURCE_DATA_ENTRY { ULONG OffsetToData; ULONG Size; ULONG CodePage; ULONG Reserved; } IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;OffsetToData和Size这两个域表示了当前资源数据的位置和尺寸。既然这一信息主要是在应用程序装载以后由函数使用的,那么将OffsetToData作为一个相对虚拟的地址会更有意义一些。——幸甚,恰好是这样没错。非常有趣的是,所有其它的偏移量,比如从目录入口到其它目录的指针,都是相对于根结点位置的偏移量。
//WINUSER.H /* * 预定义的资源种类 */ #define RT_CURSOR MAKEINTRESOURCE(1) #define RT_BITMAP MAKEINTRESOURCE(2) #define RT_ICON MAKEINTRESOURCE(3) #define RT_MENU MAKEINTRESOURCE(4) #define RT_DIALOG MAKEINTRESOURCE(5) #define RT_STRING MAKEINTRESOURCE(6) #define RT_FONTDIR MAKEINTRESOURCE(7) #define RT_FONT MAKEINTRESOURCE(8) #define RT_ACCELERATOR MAKEINTRESOURCE(9) #define RT_RCDATA MAKEINTRESOURCE(10) #define RT_MESSAGETABLE MAKEINTRESOURCE(11)在树的第一层级,以上列出的MAKEINTRESOURCE值被放置在每个种类入口的Name处,它标识了不同的资源种类。
// WINNT.H typedef struct _IMAGE_RESOURCE_DIR_STRING_U { USHORT Length; WCHAR NameString[1]; } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;这个结构仅仅是由一个2字节长的Length域和一个UNICODE字符Length组成的。
// PEFILE.C int WINAPI GetListOfResourceTypes(LPVOID lpFile, HANDLE hHeap, char **pszResTypes) { PIMAGE_RESOURCE_DIRECTORY prdRoot; PIMAGE_RESOURCE_DIRECTORY_ENTRY prde; char *pMem; int nCnt, i; /* 获得资源树的根目录 */ if ((prdRoot = (PIMAGE_RESOURCE_DIRECTORY)ImageDirectoryOffset (lpFile, IMAGE_DIRECTORY_ENTRY_RESOURCE)) == NULL) return 0; /* 在堆上分配足够的空间来包括所有类型 */ nCnt = prdRoot->NumberOfIdEntries * (MAXRESOURCENAME + 1); *pszResTypes = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt); if ((pMem = *pszResTypes) == NULL) return 0; /* 将指针指向第一个资源种类的入口 */ prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot + sizeof (IMAGE_RESOURCE_DIRECTORY)); /* 在所有的资源目录入口类型中循环 */ for (i = 0; i < prdRoot->NumberOfIdEntries; i++) { if (LoadString(hDll, prde->Name, pMem, MAXRESOURCENAME)) pMem += strlen(pMem) + 1; prde++; } return nCnt; }这个函数将一个资源种类名称的列表写入了由pszResTypes标识的变量中。请注意,在这个函数的核心部分,LoadString是使用各自资源种类目录入口的Name域来作为字符串ID的。如果你查看PEFILE.RC,你会发现我定义了一系列的资源种类的字符串,并且它们的ID与它们在目录入口中的定义完全相同。PEFILE.DLL还有有一个函数,它返回了.rsrc段中的资源对象总数。这样一来,从这个段中提取其它的信息,借助这些函数或另外编写函数就方便多了。
// 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域表示模块中有多少导出的函数以及这些函数的名称。
// 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之中了。
// PEFILE.H typedef struct tagImportDirectory { DWORD dwRVAFunctionNameList; DWORD dwUseless1; DWORD dwUseless2; DWORD dwRVAModuleName; DWORD dwRVAFunctionAddressList; } IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;和其它段的数据目录不同的是,这个是作为文件中的每个导入模块重复出现的。你可以将它看作模块数据目录列表中的一个入口,而不是一个整个数据段的数据目录。每个入口都是一个指向特定模块导入信息的目录。
//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文件格式中研究的行为。
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等等。
// 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函数一样,这一函数依靠每个信息列表的末端来获得一个置零的入口。这在种情况下,函数名称列表就是以零结尾的。
// 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文件格式任何结构化的数据段之中。这就是映像文件中最合适的位置,映像名称则肯定会出现在这里。如果映像导出了信息,那么导出数据段也会包含这一映像名称。
//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函数一样简单。
// 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了。现在,你可以享受我为你带来的这一切了!
小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)
GMT+8, 2024-11-22 09:22
Powered by Discuz! X3.4
© 2001-2023 Discuz! Team.