可执行文件格式是操作系统本身执行进制的反映,可执行文件可以是具有不同格式的二进制文件,也可以是一个文本的脚本。可执行文件映像中包含了进程执行的代码和数据,同时也包含了操作系统用来将映像正确装入内存并执行的信息。研究可执行文件能帮助我们深入理解操作系统,对于逆向工程学习有重要的帮助。Linux系统默认的可执行文件格式是elf。Windows系统的可执行文件是PE结构。小编今天向大家分享下自己对windows PE结构的一些认识,了解了PE结构大家对于Linux下的elf文件格式的学习也会得心应手。
Windows下PE结构由以下几部分组成:Dos头,PE头,节表以及后续的.text块、.data块,.rdata块和不能映射块组成,截图如下:
PE文件结构首先是Dos头,小编把该Dos头对应的结构体 _IMAGE_DOS_HEADER用思维导图整理了出来,我们看最后一个成员 e_lfanew,指向PE头的起始位置。截图如下:
DOS头Dos头接下来后有一段空间,称为Dos Stub(长度不定)。它是Dos头与PE头间的空间。通过e_lfanew我们能够得到PE头的位置并算出该段空间的大小。接下来就是PE头(结构体为_IMAGE_NT_HEADERS),PE头有三个部分组成,具体结构体如下:
typedef struct _IMAGE_NT_HEADERS{
DWORD SIGNATURE;
IMAGE_FILE_HEADER FileHeader;(标准PE头)
IMAGE_OPTIONAL_HEADER32 OptionalHeader;(可选PE头)
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32
e_lfanew中存储的值就是PE头在文件中的偏移位置,从该位置中取出四个字节就得到SIGNATURE(DWORD为双字)的值,该值为0x00004550,ASCII码为PE00。所以我们把这个文件格式叫做PE。
接下来就是标准PE头_IMAGE_FILE_HEADER FileHeader,具体结构小编也用思维导图记录了下,截图如下:
标准PE头其中NumberOfSections该变量非常重要,对于原有exe,dll等可执行的PE结构的修改都有重大的意义,包括原有磁盘文件注入自己的代码等等。符号表学过编译原理的都知道非常重要,这里小编也不做解释。
接下来就是可选PE头_IMAGE_OPTIONAL_HEADER,该结构体中包含了许多重要的信息。截图如下:
可选PE头该结构体中有一个变量AddressOfEntryPoint,占用空间为4个字节,存储的值为程序的执行入口的RVA,也就是在内存中的相对偏移。若我们想在程序中添加自己的代码并让程序一打开就运行自己注入的这段代码,那么就要修改该变量的值,使其指向这段代码在内存中的位置。
为了更好的对该结构体中的参数做点解释,小编先分析接下来的节表,其结构体如下图所示:
节表通过标准PE头中的NumberOfSections可以知道该程序有多少节,每一节在磁盘和内存中的位置等信息都记录在对应的节表中,小编截取了部分自己写的代码,用于读取节表内容,截图如下:
读取节表内容接下来小编简单介绍下磁盘中文件加载到内存中时的拉伸过程。小编自己画了个拉伸的图如下所示:
拉伸过程上图可以发现FileBuffer和 ImageBuffer地址是错开的,也就是磁盘上的文件加载到内存中时,位置发生变化。磁盘是以0x200对齐的,内存是以 0x1000 对齐的。从可选PE头中知道“S-Dos头部+PE头部+区块表”的总大小为SizeOfHeaders,PointerToRawData指的是该区块相对与磁盘文件的偏移,这么说吧,就是在还未装载到内存中时,该区块相对Dos头起始位置的偏移。SizeOfRawData是指该块在磁盘文件中的大小,而Misc.VirtualSize是指该节中真正被使用的空间的大小,根据内存对齐来说可知道SizeOfRawData>Misc.VirtualSize 。而VirtualAddress是指该区块装载到内存时相对于其在内存中起始位置的移。可选PE头中的SizeOfImage表示磁盘映像装入内存中的总大小(可以比实际的值大,但必须是SectionAlignment的整数倍)。
因此在磁盘映像装载到内存这个拉伸过程中,涉及磁盘映像文件偏移以及内存中相对偏移的转换。关于磁盘文件偏移以及内存相对偏移的转换,对于后续研究导入表导出表等十分重要。这里小编截取了自己写的一部分代码,具体不做解释,截图如下:
FOAToRVA RVAToFOA回到我们的可选PE头,其中有一项非常重要,就是IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES],即数据目录表,该数据目录表中包括的表如下图所示:
数据目录表包含的表而且IMAGE_DATA_DIRECTORY对应的结构体如下所示:
typedef struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;//数据从哪里开始放,内存相对偏移地址
DWORD Size;//有多大
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY
数据目录项目的第一张表为导出表,表明该可执行文件提供了多少个函数给别的程序使用(动态链接库dll也有这样的功能)。 结构体中的VirtualAddress为内存相对偏移地址,要先转为文件中的相对偏移地址,然后加上可执行文件的起始地址方可得到导出表在磁盘文件中的位置。导出表有对应的结构体来记录相关的信息,截图如下:
导出表对应结构体小编用一个图来解释AddressOfNames,AddressOfNameOrdinals,AddressOfFunctions间的关系。截图如下:
代码截图如下:
读取导出表数据目录项的第二项对应的为导入表,我们经常所说的IAT HOOK就与这张表有密切联系。通过该表我们可以知道该可执行文件用了哪些程序提供的函数等等,深入了解该表,可以做到让程序调用我们自己写的函数等等,这里有很多神奇的操作,有兴趣的可以自己看看《Windows PE权威指南》这本书,结构如下:
导入表对应结构体还有一张非常重要的表,重定位表。整个Windows操作系统都是围着 PE 转。分页机制允许当一个应用程序被启动多份可以加载到同一个线性地址中,如果没有分页,低端内存被占用了,别人就不能占用了。早期没有分页,低 2G 都是物理内存。多进程如何开启?只有第一个应用程序用第 1M,第二个应用程序用第 2M,每个 exe 占的物理地址和线性地址都不一样。分页后,所有的 exe 都可以使用相同的线性地址,只是物理地址不一样。分页后,代码区不用修正,因为每修正一次,就要添加一个物理页,一百个程序就要使用一百个物理页。dll也不需要修正,系统 dll 都是由微软编写,VC6 里面有设置 dll 的加载地址,如果不设置,自动加载到 0x10000000。如果 dll 设置的地址出现冲突,代码区被修正。
PE格式不一定是 x86(CPU),可能是 ARM(CPU)。如果代码区被修正,则变量地址将被修正,pe挪动了多少位置,变量地址就挪动多少。编译器不仅建立了 4 个字节的修正表,还生成一个字节属性表,比如有些地址要修正,有些不需要。比如:0x500000 这个位置需要修正,0x500008,0x700020,0x700028,0x700038,0x800020,0x800040……一共占 30×5 个字节,要进行压缩,我们将 500000或 700000 或 800000 抽出来,加一个字节偏移修正,这样形成的表格就是重定位表。
重定位表的信息通过IMAGE_BASE_RELOCATION;结构体可以找到,截图如下:
下面是小编自己写的获取重定位表的代码截图:
获取重定位表有兴趣的可以自行移动下重定位表并进行修复,只有动手我们才可以对重定位表有一个更深刻的理解。
PE结构中还有很多非常重要的信息,我们可以添加节,移动节,通过更多的操作来向这些可执行结构添加自己想要让它执行的动作。小编能力有限,因此无法做到组织正确的语言来表达,可能会有表达上的错误或是知识错误,也请多多包涵。小编自己用思维导图将整个PE结构的具体信息都画了出来,由于还未查看是否存在错误,也暂时不分享给大家了。
网友评论