滴水三期:day28.1-PE头字段说明

一、PE头字段

  • PE头字段 = DOS头 + PE标记 + 标准PE头 + 可选PE头

    我们今天分析一下头字段中所有数据的含义

二、DOS头

1.DOS头结构

struct _IMAGE_DOS_HEADER {
    0x00 WORD e_magic;  *
    0x02 WORD e_cblp;
    0x04 WORD e_cp;
    0x06 WORD e_crlc;
    0x08 WORD e_cparhdr;
    0x0a WORD e_minalloc;
    0x0c WORD e_maxalloc;
    0x0e WORD e_ss;
    0x10 WORD e_sp;
    0x12 WORD e_csum;
    0x14 WORD e_ip;
    0x16 WORD e_cs;
    0x18 WORD e_lfarlc;
    0x1a WORD e_ovno;
    0x1c WORD e_res[4];
    0x24 WORD e_oemid;
    0x26 WORD e_oeminfo;
    0x28 WORD e_res2[10];
    0x3c DWORD e_lfanew;  *
};

2.e_magic

  • “MZ标记” :用于判断是否为可执行文件

    即如果显示4D 5A,说明该文件是一个可执行文件:.sys/.dll/.exe等

3.e_lfanew

  • NT头(PE签名)相对于文件首地址的偏移,用于定位PE文件,即此PE文件真正的PE结构开始的地址

    值是不确定的,DOS头结尾到真正PE开始地址之间有空隙,上一章笔记说过是编译器留下的空间填充一些说明信息或者垃圾,内容任意

三、PE标记

  • 即e_Ifanew指向的地址,就是一个PE的标记或者叫签名

    DWORD Signature;
    
  • 所以一个可执行文件应该满足"MZ"标记和"PE"标记,如果这两点不满足可能被修改过,或者就不是一个可执行文件

四、标准PE头

1.标准PE头结构

struct _IMAGE_FILE_HEADER {
    0x00 WORD Machine;  *
    0x02 WORD NumberOfSections;  *
    0x04 DWORD TimeDateStamp;  *
    0x08 DWORD PointerToSymbolTable;
    0x0c DWORD NumberOfSymbols;
    0x10 WORD SizeOfOptionalHeader;  *
    0x12 WORD Characteristics;  *
};

2.Machine

  • 程序运行平台:支持的CPU型号,如果是0x0表示能在任何处理器上执行;如果是0x14C表示能在386及后续处理器上执行

3.NumberOfSections

  • 文件中存在的节的总数:如果要新增节或者合并节,就要修改这个值。大小表示不包括DOS头、NT头、节表,此文件分为几个节(例如.text、.idata等)

    滴水三期:day28.1-PE头字段说明_第1张图片

4.TimeDateStamp

  • 时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的

  • 时间戳的使用案例:比如现在要给一个.exe文件加壳,有些加壳软件不光要提供给它.exe文件,还需要其对应的.map文件,这个文件中记录了此.exe文件中的所有的函数的名字、地址、参数等信息,即exe文件中所有函数的描述。这两个文件编译器编译时一起生成,那么map和exe的时间戳就是一致的,如果现在这个.exe文件需要反复修改,那么exe文件的时间戳就会变,但是map的时间戳没有更新,导致exe和map不同步,如果加壳子还是按照map中的记录信息去加壳,就可能出现错误。所以很多加壳软件在加壳之前会检查exe和map文件的时间戳是否一致

5.SizeOfOptionalHeader

  • 可选PE头的大小:32位PE文件默认E0h,64位PE文件默认为F0h(大小可以自定义)

6.Characteristics

  • 特征:16位的每个位都表示不同的特征,可执行文件值一般都是0x010F,即读完后16位二进制数中从低到高第1、2、3、4、9位上的值为1,其他位为0

  • characteristic每一位表示的含义如下图:

    滴水三期:day28.1-PE头字段说明_第2张图片

    把内存中的值读出来后,即读作0x010F,此时化成二进制,第七位省略,其他的每一位都表示一个特征,如果为1,则表示此文件有此位对应的特征;为0表示没有此特征

五、可选PE头

1.可选PE头结构

struct _IMAGE_OPTIONAL_HEADER {
    0x00 WORD Magic; *
    0x02 BYTE MajorLinkerVersion;
    0x03 BYTE MinorLinkerVersion;
    0x04 DWORD SizeOfCode; *
    0x08 DWORD SizeOfInitializedData; *
    0x0c DWORD SizeOfUninitializedData; *
    0x10 DWORD AddressOfEntryPoint; *
    0x14 DWORD BaseOfCode; *
    0x18 DWORD BaseOfData; *
    0x1c DWORD ImageBase; *
    0x20 DWORD SectionAlignment; *
    0x24 DWORD FileAlignment; *
    0x28 WORD MajorOperatingSystemVersion;
    0x2a WORD MinorOperatingSystemVersion;
    0x2c WORD MajorImageVersion;
    0x2e WORD MinorImageVersion;
    0x30 WORD MajorSubsystemVersion;
    0x32 WORD MinorSubsystemVersion;
    0x34 DWORD Win32VersionValue;
    0x38 DWORD SizeOfImage; *
    0x3c DWORD SizeOfHeaders; *
    0x40 DWORD CheckSum; *
    0x44 WORD Subsystem;
    0x46 WORD DllCharacteristics;
    0x48 DWORD SizeOfStackReserve; *
    0x4c DWORD SizeOfStackCommit; *
    0x50 DWORD SizeOfHeapReserve; *
    0x54 DWORD SizeOfHeapCommit; *
    0x58 DWORD LoaderFlags;
    0x5c DWORD NumberOfRvaAndSizes; *(后面深入的重点,现在不讲)
    0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};

2.Magic

  • 说明文件类型:如果值为0x010B,表示是32位下的PE文件;如果值为0x020B,表示是64位下的PE文件

3.SizeOfCode

  • 所有代码节大小的和,必须是FileAlignment的整数倍

    比如文件只有一个代码节,大小为100h字节,如果文件对齐粒度是200h,那么会补0填充够200h字节,所以会显示200h(编译器填的);若文件有两个代码节,两个都是10字节,这个值应为400h。但是计算机发展到现在已经不使用这个值了,改了也没事,删除程序也可以正常运行,但是现在之所以保留下来是因为向下兼容,以前的软件程序发布已经遵循了这个格式,如果现在修改了,那全世界以前发布的.exe等文件都需要删除这几位

4.SizeOfInitializedData

  • 已初始化数据大小的和,必须是FileAlignment的整数倍:编译器填的,目前计算机已经不使用这个值了,删掉也可以,保留下来只是为了向下兼容

5.SizeOfUninitializedData

  • 未初始化数据大小的和,必须是FileAlignment的整数倍:编译器填的,现在没用了

6.AddressOfEntryPoint

  • 程序入口点OEP(程序真正执行的起始地址):这个值是偏移量,而不是真正运行在内存中的程序入口地址。需要再加上加载到内存的基址(imagebase),才是程序运行在内存中(4GB虚拟内存)的程序入口。这个值不是确定的

    注意:程序入口在默认情况下一般都在.code代码节当中,且OEP不是只能在.code代码节开始的位置,可以从此节当中的任何合理位置开始,也可以在其他节(如.text等)的任意合理位置开始。OEP可以人为修改,但是最后一定要让.exe文件能运行起来

  • 注意:程序入口不能理解为C语言的main函数,那只是我们写的代码的执行入口,因为在main函数被调用前还做了很多事情,所以OEP一定是.exe双击开始运行时程序开始的那个地址,可以用OD打开看一下,如下

  • 内存中的程序入口地址:使用OD打开文件**(完全模拟文件运行时加载到内存中的状态,不是硬盘上的状态)。所以OD打开一个可执行文件后,会在程序入口地址处设置断点,让程序停下来,这里就是文件在内存中真正的入口点。即文件装入到4GB虚拟内存中的起始基地址 +相对于文件首地址的偏移的程序入口地址,即imagebase + AddressOfEntryPoint**

    滴水三期:day28.1-PE头字段说明_第3张图片

7.BaseOfCode

  • 代码开始的基址:即PE文件装入内存后,所有代码的起始地址。编译器填的,可以改,不影响程序执行

    不要和程序入口混为一谈,不是代码一有,程序就要执行,程序入口可以设定到任何地方,只是在没有修改过PE结构的情况下程序入口一般都在.code代码节当中的某一个位置。如果要自己修改,比如修改到数据中,最后一定要能让程序能运行起来,不能胡改,不然没有意义。

8.BaseOfData

  • 数据开始的基址:编译器填的,程序运行用不到,想改就改

9. ImageBase

  • 内存镜像基址:我们知道每一个.exe程序都有属于自己的4GB虚拟内存,这个值就是当程序运行装入到自己的虚拟4GB内存中后的文件的起始位置。imagebase一般都是0x00400000,不能超过0x80000000,因为我们写的程序的数据只能在内存的2GB用户区中,不能占用2GB系统区

  • 计算文件运行装载到4GB内存中的某个值,就使用imagebase加上偏移量的方式来计算;

  • 为什么imagebase不从0开始?

    • 因为内存保护!我们前面学过,free一个动态分配内存的指针后,一定要将指针 = NULL,那么指针等于NULL后,这个指针指向的地址就是0x0,那么如果此时访问此指针指向的数据,或者向后偏移一定大小的范围内的数据,编译器会立马报错。所以4GB内存中开始空出来一些内存空间就是为了内存保护的
    • 因为查找效率更高。可以理解为模块对齐粒度
  • 模块的概念一个.exe文件是不是就是一个PE文件呢?不是!!!可以看成一个.exePE文件是由一堆PE文件组成的,.exe本身是一个PE文件,满足PE结构,但是.exe中可能还用到了很多.dll,每一个.dll也是一个PE文件,也满足PE结构,这些.dll有自己的功能和作用,拼凑到一个.exe文件中,.exe文件就有了完整的功能。所以相当于很多PE文件在一个PE文件中。又称.exe文件有很多模块构成,每一个.dll都是一个模块

  • 模块举例:我们使用OD打开飞鸽的.exe文件,点击上面的E按钮,可以看到一个ipmsg.exe可执行文件中包含了很多.dll的可执行文件,这些每一个.dll都是ipmsg.exe的一个模块,这些.dll模块也满足PE文件结构,但是这些模块都是.exe的一部分,所以用的还是ipmsg.exe分配的4GB内存

    滴水三期:day28.1-PE头字段说明_第4张图片
    • 我们点OD上方的M按钮,看看是否一个.exePE文件中还有很多PE文件(.dll),可以发现一个.exePE文件中不仅只有一个PE结构,.exe当中的每一个.dll文件都满足PE结构

      滴水三期:day28.1-PE头字段说明_第5张图片
    • 我们再进入到一个.dll的PE结构中看看数据,比如进入到apphelp.dll在.exe4GB内存的数据。双击apphelp.dlld的PE header,我们可以看到满足DOS头,根据DOS头最后4字节数据,再看看NT头

      滴水三期:day28.1-PE头字段说明_第6张图片
    • NT头中的内容:我们可以看到也满足,而且apphelp.dll的AddressOfEntryPoint值为0x38870;imageBase的值为0x6F8F0000,表名如果.exe运行装入到内存中时,.exe当中的apphelp.dll模块在内存中的程序入口应该在0x6F8F0000 + 0x38870 = 0x6F928870,由于OD打开后就是运行状态,所以我们通过OD查看一下apphelp.dll模块在内存中的实际起始地址是否为0x6F928870,确实如此。

      滴水三期:day28.1-PE头字段说明_第7张图片屏幕截图 2021-12-21 210158

10.SectionAlignment

  • 内存对齐:可执行文件运行时装入4GB虚拟内存中的对齐粒度,一般为0x1000字节

11.FileAlignment

  • 文件对齐:可执行文件在硬盘的对齐粒度,一般为0x200字节,还有的是0x1000字节,和内存对齐粒度相等

  • 举例说明内存对齐和文件对齐:(day27.1-PE结构概况中详细说明过)

    滴水三期:day28.1-PE头字段说明_第8张图片

12.SizeOfImage

  • 内存中整个PE文件的映射尺寸:即文件运行时装入4GB虚拟内存后的整个文件数据大小(要考虑内存对齐)。可以比实际的值大(比如文件对齐为0x200,内存对齐为0x1000时),但必须是SectionAlignment的整数倍

13.SizeOfHeaders

  • 所有头+节表按照文件对齐后的大小:即DOS头 + 垃圾数据 + PE签名 + 标准PE头 + 可选PE头 + 节表,按照文件对齐后的大小。必须是FileAlignment的整数倍,否则加载会出错

  • 举例:假如一个可执行文件的所有头和节表加起来大小为0x1800字节,但是SizeOfHeaders的值应该为0x2000,因为要满足文件对齐粒度0x1000

    滴水三期:day28.1-PE头字段说明_第9张图片

14.CheckSum

  • 校验和:一些系统文件、驱动文件等有要求,用来判断文件是否被修改(但是可以修改这个值)
  • 校验方法举例:把PE文件中的所有数据,两个两个字节相加(十六进制),最后将所有两个两个相加的结果再求和得到一个数,存放到checksum表示的4字节内存中,如果超过了4字节表示范围自然溢出即可

15.SizeOfStackReserve

  • 初始化时保留的栈大小 (最大值)

16.SizeOfStackCommit

  • 初始化时实际提交的栈大小

17.SizeOfHeapReserve

  • 初始化时保留的堆大小 (最大值)

18. SizeOfHeapCommit

  • 初始化时实际提交的堆大小

19.NumberOfRvaAndSizes

  • 目录项数目(后面学习的重点)

  • 这一字段如果是0x10,表示下面的_IMAGE_DATA_DIRECTORY DataDirectory[16]结构体有16个,一个占8字节

    滴水三期:day28.1-PE头字段说明_第10张图片

六、可执行文件的读取到装入内存过程

0.编译器生成.exePE文件

  • 举个例子,比如我们使用VC,最后按F7,编译器会编译生成对应的.exe可执行文件,此时编译器就会帮我们计算,生成PE文件的所有数据,比如imagebase或者OEP等全部字段信息,最后将.exe文件保存到硬盘中

1.文件数据读到FileBuffer

  • FileBuffer:通过winhex或者十六进制编译器打开一个存储在硬盘上的可执行文件,打开后显示的数据就是文件在硬盘上的状态。此过程只是将文件在硬盘上时的数据原封不动的复制一份到内存(FileBuffer)中,我们称这块内存叫FileBuffer,通过软件显示出来。此时文件的格式还不具备windows运行格式

    这个过程就像day26.2-C文件操作的函数中的作业,自己编写C代码,将一个.exe文件的数据从硬盘读入内存中,我们使用的十六进制显示软件原理类似

2.将文件装载到ImageBuffer

  • 将文件从FileBuffer装入ImageBuffer,即将文件对齐拉伸成内存对齐,这个过程就是将文件装入自己的4GB虚拟内存中,此过程称为PE loader。此时文件的格式基本满足windows运行格式。我们称将文件拉伸装载到的内存为imageBuffer,即内存镜像(拉伸的细节后面学习)

    对于有些文件对齐粒度=内存对齐粒度的文件可以不用拉伸

  • 此时文件在4GB虚拟内存中的起始地址,就是imagebase,一般为0x00400000,接着就可以通过imagebase + addressofentrypoint找到文件装载到内存后真正的程序入口地址;或者用imagebase加上一些偏移地址值就可以得到文件其他内容在运行时装入4GB内存后的地址

  • 如图:

    滴水三期:day28.1-PE头字段说明_第11张图片

3.操作系统将虚拟地址转化成物理地址

  • 上面的两个FileBuffer和ImageBuffer提到的所有地址,其实都是虚拟地址,我们学过操作系统知道,操作系统最后还要将这些虚拟地址转换为物理地址,才是真正的装入到真实内存中。这个过程操作系统帮我们做了不需要手动做,所以现在先了解到上面两个过程即可,就是文件在硬盘上时的数据格式,复制一份到FileBuffer中显示出来;运行时文件经过PE Loader将文件拉伸,装载到ImageBuffer中
  • 所以ImageBuffer中的文件格式虽然满足了windows运行格式,但是此时这个文件还没有执行!!即还没有分配CPU,后面操作系统还要做很多事情,才能让imageBuffer中的文件真正装入实际内存中,执行起来!在imageBuffer中其实是一个4GB虚拟内存,装入imagebuffer时有一个文件被拉伸的过程,此时已经无限接近于可被执行的格式了,但是还没有执行哦

你可能感兴趣的:(逆向,堆栈,反汇编,安全,c++,内存结构)