此图非常详细,可以看到每个字段的内容和大概含义,根据此图动手实践了PE文件结构,下面给出我自己理解的简约结构:
为了更加形象生动,特把实验过程记录如下,从实验过程中我们可以发现不小细节:
使用一个简单的PE文件举例,下图是PE文件概要信息:
图中没有列出DOS头信息,直接跳到了NT头的Image_File_Header,其中Machine=014C,有四个段,可选头大小为EO;其他是一些标志位。
接下来是可选头部分image base=400000也就是镜像文件载入地址为400000,
段落对齐1000(1024字节),文件对齐200(512字节),还有一些堆栈默认大小;
接下来是可选头里面的Data Directory数据目录,这个目录表明了导入表的虚拟地址:
2244(此表在PE文件是.rdata段,地址:E44,.rdata的地址为C00,也就是.rdata偏移244就可找到导入表,
因为.rdata虚拟地址是2000所以导入表虚拟地址为2000+244=2244),以及导出表、IAI等的虚拟地址计算方法同上。
接下来就是一个段表,里面标示了有几个段落,每个段落的PE文件地址是多少,大小多少,虚拟地址多少,虚拟地址空间中大小多少等信息。
.txt段落PE地址400,大小800,虚拟地址1000,大小7B6,为什么大小一会儿是800,一会儿是7B6呢?
7B6为段实际大小,PE文件要求段对齐为200,400+7B6 = BB6,那么下一个段起址是BB7,不符合200的倍数关系,
C00才是200的倍数,因此填充了一些无用的0使下一个段地址为200的整数倍;
这个段放的都是代码,实际映射到进程的地址空间是1000。
.rdat段,PE地址C00,大小800,虚拟地址2000,大小656。这个段放的是一些只读量,里面就有导入表中的函数虚拟地址的值。
因此修改这个值就可以HOOK API;还有字符串常量,例如char * a = "wo shi shui",这个字符串就放在这里面。
.data PE地址1400,大小200,虚拟地址3000,大小394,这里394>200是因为有些变量没有分配空间,
所以没有占磁盘空间,但是到虚拟地址中就必须为其预留空间。
也就是说PE文件将全局变量压缩存储了。这个段都是数据,有全局初始化数据和未初始化数据,有静态变量。
.rsrc PE地址1600,大小200,虚拟地址4000,大小1AC。这个段是资源段,可能用来存放图片。
在接下来就是各个具体段的数据了.txt,.rdat,.data,.rsrc,而导入表信息在.rdat里面存放着。
接下来是填充,为了让PE文件512字节对齐,从而进行填充。
然后PE文件结束了。
好了,下面来具体看看PE文件:
下图是用WINHEX打开PE文件后的实际情况:
下面是NT头开始的数据,包含了image file header、image optional header、四个段的信息表、以及四个段的具体数据
上图已经圈出了头部的数据结构,每个颜色代表一个头,最下面是各段的具体数据,当然首先是被全0填充的,直到400才开始是真实的段数据。
测试代码如下:,此代码最终生成dlltest.exe文件,并调用test.dll,主要是打印出EXE所在的堆栈地址,DLL和EXE如何协作。
#include "stdafx.h" #include "../TestDelayDll/TestDelayDll.h" char abc; int cba = 0x0000a0a0; int _tmain( int argc, _TCHAR* argv[ ] ) { int b = 0x1111; char *ok = "wo shi shui"; char *xx = new char[4096]; printf( "[exe]no-init=%X, init=%X, stack=%X, heap=%X, %X:%s ", &abc, &cba, &b, xx, ok, ok ); //load fnTestDelayDll( ); delete []xx; getchar( ); return 0; } test.dll主要代码: int fnTestDelayDll() { int sp = 0xabcd; char *a = new char[1024]; printf("[dll]no-init=%X init=%X stack=%X heap=%X ", &nTestDelayDll, &g_testValue, &sp, a); delete []a; return 42; }
上面两段代码可能会被编译器优化掉,因此你必须关闭编译器优化才能看到真实的情况。
结果如下:
进程的内存结构如下(我自己整理的,可能不是很对,堆是各个线程公用的,栈是一个线程一个,用完还给系统):
我自己的理解是这样的,在执行一个EXE程序时,父进程负责为EXE创建进程地址空间,并将EXE文件映射到此空间,当然是以EXE文件所在的存储器为后备存储器,映射过程中是有一定流程的,首先将PE文件头(这个头包含了所有的小头)直接映射到400000开始的虚拟地址空间,然后根据头中的信息将4个段分别映射到地址空间,注意,这里的映射并不是和PE文件存储结构一致,而是根据PE头中的信息进行映射,例如.txt段在PE中偏移是400,那么按道理应该映射到400000+400=400400这是地址,可是实际上是映射到401000地址,就是因为PE头中已经指示了必须映射到此地址,其他各段以此类推。
映射完成后,系统开始找导入表,按照导入表中的信息,导入EXE依赖的DLL,例如需要导入Test.dll文件,这时候执行的映射和EXE一致,只不过地址为定位从10000000开始映射,实际上具体的系统可能不是从这边映射,因为一般我们编写的DLL都从10000000开始映射,这样2个以上的DLL会出现重叠,因此这时候系统会重定位该DLL,重定位DLL后,因为载入地址不在是10000000,所以DLL代码中所有可能引用地址的地方都需要更新,这时候系统会找到DLL的重定位段,根据重定位段记录的地址表,逐个更新地址。
对于DLL重定位我也做了一实验,让testdll.exe加载了两个DLL文件,这两个DLL文件镜像载入的虚地址都是10000000,测试发现,第一个DLL被正常映射到10000000地址,一切都是按照PE约定的虚拟地址进行映射,而第二个DLL镜像被映射到了3b0000地址,也就是被映射到了exe镜像的上面,哈哈,.txt代码段本应该是10001000的,却被映射到了3b1000地址,看来DLL镜像在映射的时候被重定位了,并且还发现,原来代码段中的内容被改掉了,果然是系统修复了地址,如图:
原始汇编:
看来DLL重定位确实会动态修改汇编代码段内容.