PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,我们比较熟悉的DLL和exe文件都是PE文件。了解PE文件格式有助于加深对操作系统的理解,掌握可执行文件的数据结构机器运行机制,对于逆向破解,加壳等安全方面方面的同学极其重要。
PE在二进制文件中就是一堆0和1,但是它是按一定的格式来存储的,下面我先从宏观上来给大家介绍。
PE分dos头、NT头(包含标准PE头和可选PE头(包含数据目录))、节表。
下面我上一张图给大家看一下:
dos头(方块1第一块),NT头(方块1第二块),标准PE头(方块2),可选PE头(方块3),数据目录(方块5)、节表(方块4)。
下面我按着这个编号来介绍。
首先我讲一下dos头,如图
微软定义如下:
虽然字段比较多,但是我们初学只要知道2个字段就ok了—— WORD e_magic和LONG e_lfanew; e_magic字段是标记,用途就是标记这个文件是什么类型的文件,比如:exe,dll,sys文件都是4D5A开头的。
MZ是一个大佬名字的简称,不过他貌似已经挂了???xxxx 反正知道mz标记就是代表可执行文件就ok了。
lfanew字段就是标记NT头在文件里的偏移,文件头+lfanew就是NT头的位置(中间有一段没用的数据叫 dos sub 下文增加节会讲)。
0x50 0x45是PE的标志 ,因为“PE”的ascll码就是0x50 0x45。
NT头包含标准PE头和可选PE头,文件的很多信息都在这里面。
Signature字段里存的就是pe标记,注意宽度是4,然后就是标准PE头。
标准PE头一共是20个字节,下面我讲一下字段的用处。
1.Machine字段,表示目标CPU 的类型。
几个常见的及其标识如下:
机器 标识
Intel I386 14ch
MIPS R3000 162h
Alpha AXP 184h
Power PC 1F0h
MIPS R4000 184h
2.NumberOfSection,标识节的数目,关于节后面会详细讲。
3.TimeDateStamp
PE文件创建的时间,这个时间是指从1970年1月1日到创建该文件的所有的秒数。
4)PointerToSymbolTable。略
5)NumberOfSymbol。略
6)SizeOfOptionalHeader:紧跟着标准PE头后面的数据大小,这也是一个数据结构,它叫做可选PE头,其大小依赖于是64位还是32位文件。32位文件值通常是00E0h,对于64位值通常为00F0h。
7)Characteristics:文件属性,普通EXE文件这个字段值为010fh,DLL文件这个字段一般是0210h。
(标准pe头)
可选PE头,其大小依赖于是64位还是32位文件。32位文件值通常是00E0h,对于64位值通常为00F0h。
可选PE头字段多,但是重要的不多(加*的都很重要,不要慌,*是我加的)。
额,重要的也蛮多。。。
详细字段的用处我会在下面代码里会详细说明,数据目录。。。。好吧,现在讲。
在PE结构中最重要的就是节表和数据目录表,数据目录一共指向16张表,分别是:导出表、导入表、资源表、异常信息表、安全证书表、重定位表、调试信息表、版权所以表、全局指针表
TLS表、加载配置表、绑定导入表、IAT表、延迟导入表、COM信息表 最后一个保留未使用。
这么多表中我们只要注重导出表、导入表、重定位表、IAT表就可以了。他们直接影响程序运行。 注意:数据目录只是各种表的地址(rva)。需要寻址才能找到他们。
(数据目录,太长了没有截完)
每张记录宽度8字节 然后*16=128字节
经过解析就变成
代码实现思路就是按照字段宽度偏移来遍历dos头、NT头。(这里rva和foa都一样)
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。
另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS 结构中的 FileHeader.NumberOfSections 字段来指定的。
一个节表的宽度是40个字节,最后一个节表后面要跟上40个字节的0,也就是结束标志。(也可以没有结束标志,不过可能会炸。。。)
(节表)
一个文件至少由1个节表组成,每个节表记录了节的地址(rva和foa),大小,权限等,每节表占40个字节。
下面来说说每一个字段和作用。
联合体就当成一个变量来用就可以了,宽度是最大的那一个变量。
1、Name 8个字节 一般情况下是以"\0"结尾的ASCII吗字符串来标识的名称,内容可以自定义.
注意:该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理.
2、Misc 双字 是该节在没有对齐前的真实尺寸,该值可以不准确。
3、VirtualAddress 节区在内存中的偏移地址。加上ImageBase才是在内存中的真正地址.
4、SizeOfRawData 节在文件中对齐后的尺寸.
5、PointerToRawData 节区在文件中的偏移.
6、PointerToRelocations 在obj文件中使用 对exe无意义
7、PointerToLinenumbers 行号表的位置 调试的时候使用
8、NumberOfRelocations 在obj文件中使用 对exe无意义
9、NumberOfLinenumbers 行号表中行号的数量 调试的时候使用
10、Characteristics 节的属性
节的属性:
[值:00000020h] [IMAGE_SCN_CNT_CODE // Section contains code.(包含可执行代码)]
[值:00000040h] [IMAGE_SCN_CNT_INITIALIZED_DATA // Section contains initialized data.(该块包含已初始化的数据)]
[值:00000080h] [IMAGE_SCN_CNT_UNINITIALIZED_DATA // Section contains uninitialized data.(该块包含未初始化的数据)]
[值:00000200h] [IMAGE_SCN_LNK_INFO // Section contains comments or some other type of information.]
[值:00000800h] [IMAGE_SCN_LNK_REMOVE // Section contents will not become part of image.]
[值:00001000h] [IMAGE_SCN_LNK_COMDAT // Section contents comdat.]
[值:00004000h] [IMAGE_SCN_NO_DEFER_SPEC_EXC // Reset speculative exceptions handling bits in the TLB entries for this section.]
[值:00008000h] [IMAGE_SCN_GPREL // Section content can be accessed relative to GP.]
[值:00500000h] [IMAGE_SCN_ALIGN_16BYTES // Default alignment if no others are specified.]
[值:01000000h] [IMAGE_SCN_LNK_NRELOC_OVFL // Section contains extended relocations.]
[值:02000000h] [IMAGE_SCN_MEM_DISCARDABLE // Section can be discarded.]
[值:04000000h] [IMAGE_SCN_MEM_NOT_CACHED // Section is not cachable.]
[值:08000000h] [IMAGE_SCN_MEM_NOT_PAGED // Section is not pageable.]
[值:10000000h] [IMAGE_SCN_MEM_SHARED // Section is shareable(该块为共享块).]
[值:20000000h] [IMAGE_SCN_MEM_EXECUTE // Section is executable.(该块可执行)]
[值:40000000h] [IMAGE_SCN_MEM_READ // Section is readable.(该块可读)]
[值:80000000h] [IMAGE_SCN_MEM_WRITE // Section is writeable.(该块可写)]
可选PE头下面就是节表,所以我们可以用dos头+lfanew+NT头的宽度来定位,注意可选pe头宽度不确定,所以只能一步一步偏移过来,下面代码我会讲。
这个文件一共有6张表,而且在标准pe头里已经记录了
当你打开一个exe时,操作系统首先会读取PE的数据,然后再把节给加载到内存里,
但是加载的时候有可能会拉升节,因为内存对齐是0x1000也就是一个页。但是老的编译器生成的PE文件的文件对齐是0x200,所以这种文件会被拉升。
PE加载的过程:
1、根据SizeOfImage的大小,开辟一块缓冲区(ImageBuffer).
2、根据SizeOfHeader的大小,将头信息从FileBuffer拷贝到ImageBuffer
3、根据节表中的信息循环讲FileBuffer中的节拷贝到ImageBuffer中.
通常每个节都会遗留一些0,这和对齐有关系,比如说文件对齐是200h,存储的数据只有10h,那么这个节里还会有190h的空间给我们随便搞。如果想在别人程序里加一段自己的程序,就可以在代码节或其他节的空隙里添加硬编码,然后修改OEP让程序一开始先执行你的程序,然后再跳到原来的OEP上。
放代码前,先来了解2个硬编码,E8,E9。
E8对应的汇编语句就是call,E9就是jmp。
E8后面跟地址(4个字节)E9也是。不过跳转的地址不能写死,有一个公式就是:
真正要跳转的地址 = E8这条指令的下一行地址 + X
X = 真正要跳转的地址 - E8这条指令的下一行地址
写代码的时候只要计算一下x,然后把x填到E8/E9后面就可以了。
//先调用文件打开函数-》文件buffer转映像buff-》添加代码函数(也就是这个)-》映像buff
//转文件buff函数存盘
这个比较简单,多余文字我不写了。直接上代码。。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-6-1 14:51
被AMask编辑
,原因: