一篇转载文章,转载于:https://www.52pojie.cn/thread-1393291-1-1.html
一个PE文件可以分为两种状态:运行态和非运行态

非运行态:当一个PE文件尚未被运行时,其数据存储在磁盘中,也就是PE个人笔记一之PE介绍中PE的状态

运行态:当一个PE文件被打开后,PE文件的相关数据将被装载到内存中,此时为运行态


在细讲PE两种状态前,回顾先前在笔记一中的相关内容:

整体结构图

image-20210316164115579

整体结构表

结构 对应C数据结构 默认占用空间大小(单位字节)
DOS MZ头 _IMAGE_DOS_HEADER 64
DOS Stub 仅在MS-DOS系统下有效,不作研究 不固定
PE文件头 _IMAGE_NT_HEADERS 4+20+224=248
PE文件头标志 Signature 4
PE文件表头/标准PE头 _IMAGE_FILE_HEADER 20
PE文件表头可选部分/扩展PE头 _IMAGE_OPTIONAL_HEADER 224
块表/节表 _IMAGE_SECTION_HEADER 40
块/节 由块表/节表决定

非运行态

回顾

先前的笔记一中只是简单介绍了非运行态下如何判断一个文件是否为PE文件

接下来讲讲为何能通过之前的方法来判断PE文件


判断PE文件的流程可概括为如下三步:

  1. 判断头2个字节是否为4D 5A(ASCII码为MZ)
  2. 找到3Ch位置数据
  3. 根据第二步中的位置数据再找到对应的地址,判断这个地址是否为50 45 00 00(对应ACSII码为PE..)

地址 长度(单位字节) 对应C的数据结构 说明 ASCII
0 2 _IMAGE_DOS_HEADER的第一个成员e_magic DOS MZ头的第一个成员 4D 5A MZ
3C 2 _IMAGE_DOS_HEADER的最后一个成员e_lfanew 指出PE头文件偏移位置 不定 不定
[3C] 4 Signature PE文件头标志 50 45 00 00 PE..

下面结合实例再来分析:

这次使用WinHex这个工具来进行查看

image-20210315210005799


此时对应的表格数据为:

地址 说明 ASCII
0 DOS MZ头的第一个成员 4D 5A MZ
3C 指出PE头文件偏移位置 F0
F0 PE文件头标志 50 45 00 00 PE..

上面对一个PE文件的判断只涉及了DOS MZ头和PE文件头中的PE文件头标志

下面继续分析其它结构


后续分析

PE文件头标志和标准PE头

从先前的PE的结构继续向后看24个字节(PE文件头标志的大小+标准PE头大小 4+20)得到扩展PE头的首地址

先前的地址为:F0

后来的地址为:0xF0+24=240+24=264=0x108

所以从F0~108为PE文件头标志和标准PE头

从108开始就是扩展PE头了

PE文件头标志和标准PE头:

image-20210315212241373


扩展PE头

从先前得到的扩展PE头地址继续向后看224个字节(扩展PE头大小)得到块表的首地址

先前的地址为:108

后来的地址为:0x108+224=264+224=488=0x1E8

所以从108~1E8为扩展PE头

从1E8开始就是块表了

扩展PE头:

image-20210315213947563


块表

从先前得到的块表头地址继续向后看40个字节(块表大小)得到第二个块表的首地址

先前的地址为:1E8

后来的地址为:0x1E8+40=488+40=528=0x210

所以从1E8~210为第一个块表

从210开始就是第二个块表了

第一个块表:

image-20210315234922585


同理可得剩下的几个块表

第二个块表:

image-20210315235048573

地址从210~238


第三个块表:

image-20210315235154581

地址从238~260


第四个块表:

image-20210315235402774

地址从260~288


第五个块表:

image-20210315235504709

地址从288~2B0


汇总块表

块名称 块地址
.text 1E8~210
.rdata 210~238
.data 238~260
.rsrc 260~288
.reloc 288~2B0

块表后的空隙

块表后面跟着的应该是块,但在块表后和块之前却多出了一段空间

这里为2B0~400

image-20210316000131381


先前的PE结构中间都没有空隙,为连续存储

但在块表和块之间是可能存在空隙的,这个空隙里一般被填充为编译器插入的数据(也可以没有,就是此时的情况)

这段空隙的修改并不会导致程序不可运行,因而可被拿来写入自己想要的代码来对程序进行修改


为什么会存在这段空隙?

这段空隙存在的原因在于块表和块并没有连续存储

所以这段空隙的存在与否及长度 取决于 块的起始位置

而块的起始位置则由扩展PE头中的某个成员决定


给出扩展PE头在C中的定义:

 复制代码 隐藏代码typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment; //<--- 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders; //<--- 决定块的起始位置
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

找到结构体中的SizeOfHeaders成员(DWORD类型占4个字节)

SizeOfHeaders的含义为3个头按照文件对齐后的大小:(DOS头大小+PE头大小+块表大小)加完的结果进行文件对齐后得到的大小

头大小相加很好理解,按照之前得到的头大小和为:2B0,于是问题就在于文件对齐

什么是文件对齐

讲到文件对齐就涉及到扩展PE头中的另一个成员:FileAlignment(DWORD类型占4个字节)

文件对齐就是要求SizeOfHeaders必须为FileAlignment的整数倍


知道了由来以后,验证一下

前面已经得知了扩展PE头的首地址为108

从108开始往后找36个字节(中间间隔了1个WORD,2个BYTE,8个DWORD,即12+21+8*4=36)

FileAlignment的地址为:0x108+36=264+36=300=0x12C

FileAlignment:

image-20210316003503508

FileAlignment为00 00 02 00=0x200(小端存储)

前面得到的头大小和为0x2B0显然不是FileAlignment的整数倍

于是将SizeOfHeaders设置为FileAlignment的整数倍:(2B0/200+1)*200=400

得出的SizeOfHeaders的大小应该为0x400


再来验证一下SizeOfHeaders的大小

前面得到的FileAlignment的地址为12C

从12C开始往后找24个字节(中间间隔了6个WORD,3个DWORD,即62+34=24)

SizeOfHeaders的地址为:0x12C+24=300+24=324=0x144

SizeOfHeaders:

image-20210316003735412

SizeOfHeaders为 00 00 04 00=0x400(小端存储)

得到的大小和计算出来的大小一致,验证完毕


为什么要文件对齐

和内存对齐一样,都是为了使执行时的效率更高,有关内存对齐可参考:逆向基础笔记十八 汇编 结构体和内存对齐

PS:上面的内存对齐为程序中局部的内存对齐,主要针对的是编程时的变量、结构体等,和后面要讲的内存对齐要区别开


块的起始地址由块表中的PointerToRawData决定,第一个块的起始地址则由上面的SizeOfHeaders决定

块部分存储的为数据,如何存储由块表决定,这里主要探讨每个块的 起始地址、块大小、结束地址,其它留作之后的笔记

给出块表在C中的定义

 复制代码 隐藏代码#define IMAGE_SIZEOF_SHORT_NAME              8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData; //<--- 块的大小
DWORD PointerToRawData; //<--- 块在磁盘文件中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

块的起始地址

找到结构体中的PointerToRawData成员(DWORD类型占4个字节)

PointerToRawData的含义为该块在磁盘文件中的偏移

前面已经知道第一个块表的首地址为1E8

从1E8开始往后找20个字节(中间间隔了1个BYTE[8],3个DWORD,即18+34=20)

PointerToRawData的地址为:0x1E8+20=488+20=508=0x1FC

PointerToRawData:

image-20210316011603629

PointerToRawData为00 00 04 00=0x400

和通过SizeOfHeaders得到的一致,验证了第一个块的PointerToRawData由SizeOfHeaders决定

块的大小

SizeOfRawData为块的大小(文件对齐后)

SizeOfRawData就在PointerToRawData前面

所以其地址为:PointerToRawData地址-4=0x1FC-4=0x1F8

SizeOfRawData:

image-20210316152420615

SizeOfRawData为00 92 19 00=0x199200

块的大小和前面三个头(DOS部首+PE文件头+块表)的大小一样,也要满足文件对齐

先前得到的FileAlignment为0x200,这里的SizeOfRawData:0x199200为FileAlignment的整数倍,满足文件对齐


块的结束地址(下一个块的起始地址)

块的结束地址为块的起始地址+块的大小

即块的结束地址=0x400+0x199200=199600

image-20210316153738194


可以看到第一个块和第二个块之前是存在空隙的,这段空隙也是由于文件对齐产生的


小总结

在非运行态下:

  • DOS部首和PE文件头及块表连续存储,中间没有空隙
  • 而块表和块之间由于文件对齐可能会存在空隙
  • 块和块之间也由于文件对齐可能会存在空隙

相关数据结构成员:

数据结构成员 所属数据结构 说明
SizeOfHeaders 扩展PE头 头大小(文件对齐后)
FileAlignment 扩展PE头 文件对齐
PointerToRawData 块表 第一个块表的PointerToRawData由SizeOfHeaders决定,后面块表的PointerToRawData由前一个块表的PointerToRawData+SizeOfRawData决定
SizeOfRawData 块表 块表的大小(文件对齐后)

记录一下各结构的起始和结束位置,方便和运行态进行比较

结构 起始地址 结束地址 大小
DOS部首 0 F0 0xF0=240
PE文件头 F0 1E8 0xF8=244=224+40
块表 1E8 2B0 0xC8=200=5*40
前三个结构 0 400 0x400(文件对齐后)
第一个块 400 199600 0x199200(文件对齐后)

运行态

前面介绍了非运行态(硬盘状态)下PE文件的结构,现在看看运行态(内存状态)下PE文件的结构

加载运行态的PE文件

1.启动PE文件

image-20210316132316337


2.然后返回WinHex,点击工具→打开RAM(R)… 或直接使用快捷键Alt+F9

image-20210316132406704


3.打开后显示如下:

image-20210316132549551

选中我们要分析的PE文件,使其展开


4.展开后显示如下:

image-20210316132819269

选中.exe打开


5.最后确定即可

image-20210316133031067


分析运行态的PE文件

按照先前分析的流程,再分析一遍运行态下的PE文件

DOS部首和PE头标志

image-20210316134218062

此时的DOS部首起始地址为400000,结束地址为4000F0


PE文件头标志和标准PE头

image-20210316134123745

此时PE文件头的起始地址为4000F0,结束地址为400108


扩展PE头

image-20210316134503765

此时PE文件头的起始地址为400108,结束地址为4001E8


块表

image-20210316134642926

此时块表的起始地址为4001E8,结束地址为4002B0


块表后的空隙

从前面的分析来看,在块表前的结构在运行态和非运行态除了起始地址不同以外,其它地方并无不同

起始地址的由来等相关内容留作之后的笔记说明

按照经验,块表后的空隙也应该是持续到先前的400+400000=400400

于是前往查看:

image-20210316135241232


发现并非如此,400400仍然为空隙

先前分析得知在非运行态块表后的空隙是因为文件对齐产生的

而在运行态中,显然就不是由文件对齐决定的


接着向下查看,找到块的起始位置

image-20210316135922474


发现此时块的起始位置为401000,偏移为1000,而不是 非运行态的400

于是熟悉的问题又回来了:

为什么会存在这段空隙?

和先前的文件对齐类似,当程序处于运行态时,会有另一种对齐方式:内存对齐

和内存对齐相关的属性和文件对齐(FileAlignment)类似,也是取决于扩展PE头中的一个成员


再次给出扩展PE头在C中的定义:

 复制代码 隐藏代码typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment; //<--- 内存对齐
DWORD FileAlignment; //<--- 文件对齐
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders; //<--- 决定块的起始位置
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

找到结构体中的SectionAlignment成员,会发现它就在FileAlignment(文件对齐)成员的上面

知道了由来以后,验证一下

前面已经得知了扩展PE头的首地址为400108

从400108开始往后找32个字节(中间间隔了1个WORD,2个BYTE,7个DWORD,即12+21+7*4=32)

SectionAlignment的地址为:0x400108+32=0x400000+264+32=0x400000+296=0x400128

SectionAlignment:

image-20210316141510008

SectionAlignment为00 10 00 00=0x1000(小端存储)

前面得到的头大小和为0x2B0显然不是SectionAlignment的整数倍

头大小应该设置为SectionAlignment的整数倍:(2B0/1000+1)*1000=1000

PS:这里的头大小不会被设置到SizeOfHeaders,因为SizeOfHeaders为文件对齐专用

在非运行态中,块的起始位置由PointerToRawData决定,且PointerToRawData必须为FileAlignment的整数倍

但在运行态中,块的起始位置则并不由PointerToRawData决定,PointerToRawData和SizeOfHeaders一样都为文件对齐专用

运行态块存储涉及内容较多,这里只查看一下第一个块的起始地址、结束地址和大小,不作具体探究,其它留作之后的笔记

块的起始地址

第一个块的起始地址取决于(DOS部首+PE文件头+块表)的总大小进行内存对齐后的结果

image-20210316162649154

第一个块的起始地址=0x400000+0x1000=0x401000


块的结束地址

image-20210316162347979

第一个块的结束地址=0x59B000


块的大小

块的大小=块的结束地址-块的起始地址=0x59B000-0x401000=0x19A000(满足内存对齐)


运行态时,块的大小满足内存对齐,非先前的文件对齐


小总结

在运行态下:

  • DOS部首和PE文件头及块表连续存储,中间没有空隙
  • 而块表和块之间由于内存对齐可能会存在空隙
  • 块和块之间也由于内存对齐可能会存在空隙

相关数据结构成员:

数据结构成员 所属数据结构 说明
SectionAlignment 扩展PE头 内存对齐

各结构的起始和结束位置:

结构 起始地址 结束地址 大小
DOS部首 400000 4000F0 0xF0=240
PE文件头 4000F0 4001E8 0xF8=244=224+40
块表 4001E8 4002B0 0xC8=200=5*40
前三个结构 400000 401000 0x1000(内存对齐后)
第一个块 401000 59B000 0x19A000(内存对齐后)

对比PE两种状态

本笔记主要针对PE两种状态中的文件对齐和内存对齐进行比较,其它的内容暂时没有涉及,将在后续笔记里陆续提到

相同点

无论是在运行态还是在非运行态,DOS部首、PE文件头、块表块表均为连续存储,中间没有空隙

第一个块表的首地址都受DOS部首大小+PE文件头大小+块表大小影响,都需要对齐

块和块之间也都需要对齐

不同点

运行态和非运行态的起始地址不同

在非运行态中,块表和块之间的空隙由文件对齐产生,块和块之间的空隙由文件对齐产生

在运行态中,块表和块之间的空隙由内存对齐产生,块和块之间的空隙由内存对齐产生


非运行态和运行态映射图

image-20210316163800340

附件

附上本笔记中分析的EverEdit文件:点我下载