分享
 
 
 

PE文件格式详解(6)

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

导出数据段,.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是一个相对虚拟地址,它指向一个虚拟地址表。在文件装载的时候,这个虚拟地址表会被装载器置于段数据之中。但是在文件装载前,这些虚拟地址会被一些严密符合函数名称列表的虚拟地址替换。所以在文件装载之前,有两个同样的虚拟地址列表,它们指向导入函数列表。(未完待续)

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有