首先,我们回顾一下上篇中的PE文件的主要结构如下图;
然后,我们从 细节上来了解一下PE文件结构;
1.DOS部分
DOS部分包含了两部分,第一部分是IMAGE_DOS_HEADER结构体,我们称其为DOS MZ文件头,装了vs2013的话这个结构体在C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include\WinNT.h (注:其他PE结构体也都在这个文件里)中定义了,其他版本可以类比目录结构进行查找,或者直接在电脑上搜索,其结构体(其大小为64个字节,可以自己数一下,也可以写个程序测一下,同样可以看下面结构体中给的偏移)如下,为了方便使用我们在前面加上了偏移:
下面我们用winhex打开记事本notepad.exe这个程序来看一下DOS MZ头部长什么样,如下图:
接下来是DOS块,这段数据长度是不确定的,主要给链接器用,在Windows下这块被删掉也不影响程序运行,虽然大小不固定,但是也有办法来确定其大小,我们从IMAGE_DOS_HEADER结构体上可以看出,其最后一个成员指向了PE头部,那么就可以知道DOS Stub部分就在DOS头部到PE头部标记之间,如下图():
2.PE文件头
PE文件头由三部分组成,先从结构体来看一下它长啥样:
我们再看看IMAGE_FILE_HEADER结构体,如下:
我们看一下Signature和FileHeader部分的二进制:
接下来我们看看IMAGE_OPTIONAL_HEADER32结构体,其大小不是固定的,一般32位大小为0x00E0,64位为0x00F0,其结构体如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
0x18h WORD Magic; //标志字,(ROM映像0107h),,普通可执行 //文件(010Bh)
0x1Ah BYTE MajorLinkerVersion; //连接程序主版本号
0x1Bh BYTE MinorLinkerVersion; //连接程序福版本号
0x1Ch DWORD SizeOfCode; //所有代码区块的总大小
0x20h DWORD SizeOfInitializedData; //所有已初始化数据的总大小
0x24h DWORD SizeOfUninitializedData; //所有未初始化数据的总大小
0x28h DWORD AddressOfEntryPoint; //程序执行入口RVA
0x2Ch DWORD BaseOfCode; //代码区块起始RVA
0x30h DWORD BaseOfData; //数据区块起始RVA
//以下属于NT结构增加的领域
0x34h DWORD ImageBase; //程序首选装载地址
0x38h DWORD SectionAlignment; //内存中区块的对齐值大小
0x3Ch DWORD FileAlignment; //文件中区块的对齐值大小
0x40h WORD MajorOperatingSystemVersion; //要求操作系统最低主版本号
0x42h WORD MinorOperatingSystemVersion; //要求操作系统的最低福版本号
0x44h WORD MajorImageVersion; //镜像主版本号
0x46h WORD MinorImageVersion; //镜像福版本号
0x48h WORD MajorSubsystemVersion; //最低子系统主版本号
0x4Ah WORD MinorSubsystemVersion; //最低子系统福版本号
0x4Ch DWORD Win32VersionValue; //保留,必须为0(没有被病毒感染时)
0x50h DWORD SizeOfImage; //映像装入内存的总尺寸(内存对齐 //的倍数)
0x54h DWORD SizeOfHeaders; //所有头加区块表的大小
0x58h DWORD CheckSum; //映像校验和
0x5Ch WORD Subsystem; //可执行文件期望的子系统
0x5Eh WORD DllCharacteristics; //DllMain何时被调用
0x60h DWORD SizeOfStackReserve;//初始化时栈的大小
0x64h DWORD SizeOfStackCommit; //初始化时实际提交栈的大小
0x68h DWORD SizeOfHeapReserve; //初始化时保留的堆大小
0x6Ch DWORD SizeOfHeapCommit; //初始化时实际提交栈的大小
0x70h DWORD LoaderFlags; //与调试有关,默认为0
0x74h DWORD NumberOfRvaAndSizes;//下边数据目录的项数
0x78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表
} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;
接下来我们在二进制中找到它,如下:
3.节表
节表非常重要,我们程序真正的数据存放到一个一个的节中,我们有多少节,每个节从哪里开始,哪里结束,里面存放的是什么数据,都是由节表来记录的,同样我们先来看一下其结构体长什么样,如图:
接下来我们看看二进制中节表是什么样,一个节表40个字节,如图:
从上图可以看到它有五个节。
4.节
那我们找到了前三个部分,而且前三个部分都是连续的,那第四部分的节从哪找呢?这时我们需要了解一个IMAGE_OPTIONAL_HEADER32字段 SizeOfHeaders,这个字段存的就是DOS头部PE头部和节表大小相加后按文件对齐值对齐后的大小,比如他们相加是0x354个字节,但是这个字段里存的一定不会是0x354这个数据,因为它是按文件对齐后的大小。那么什么是文件对齐值?
同样在扩展头结构中有一个FileAlignment字段用来存放文件对齐值,其可能值有可能是0x200或0x1000,如果这个值是0x200这SizeOfHeaders中存的就会是0x400,如果FileAlignment值是0x1000,那么SizeOfHeaders中存的就会是0x1000,其思想类似C中结构体对齐,以空间换时间。
接下来我们到分析的文件中找找这个值,FileAlignment由上面给的结构体,我们已经标识出其相对PE标识的偏移值是0x3C,如图:
从上图PE标识处偏移三行,再偏移C字节找到FileAlignment,从其值可以看到,这个值就是0x200(看不懂了解一下大小端),那么SizeOfHeaders字段的值一定是0x200的整数倍,就是以0x200的整数倍向上取整,接下来我们找一下SizeOfHeaders的值,从上面结构体中我们可以看出,相对PE标识偏移0x54,如图:
可以看出其大小时0x400,就是0x200的整数倍。那么中间如果有空闲的地方有啥用,其实没啥用,哪些空白你想怎么改怎么改;第一个节开始的地方就是紧接着头大小后面的部分;同样这种文件对齐也适用于节,当大小不满足的时候在后面填0对齐。
最后,我们来讲解本篇的主题
通过上面的讲解,我们再来看下面的图就一目了然了:
从图中,我们可以看出,一个PE文件,在硬盘中的状态和在内存中状态是不一样的,在文件中要考虑文件对齐,在内存中要考虑内存对齐,接下来我们将notepad.exe运行起来,用winhex点击工具,然后选择在内存中打开,选择notepad.exe代开,然后我们就可以对比其有什么不同了,我们先来看看其二进制,左边为文件中的notepad.exe,又边为内存中的,如图:
我们可以看到除了偏移外,其值都是一样的,那么后面也都是一样的吗?
不一样,在文件中第一个节是从0x400开始的,如图:
在内存中,第一个节从起始位置偏移0x1000处开始:
为什么没有从0x400处开始,原因是由内存对齐值决定的,在扩展头中我们找一下内存对齐字段SectionAlignment,如下图:
从这里我们可以看出它的值是0x1000,现在我们清楚内存中为什么偏移了0x1000了。
附录
提供一个打印各个结构体大小的C++程序:
#include <windows.h>
#include <winNT.h>
#include <iostream>
using namespace std;
int main()
{
cout << "IMAGE_DOS_HEADER: " << sizeof(IMAGE_DOS_HEADER) << endl;
cout << "IMAGE_NT_HEADERS32: " << sizeof(IMAGE_NT_HEADERS32) << endl;
cout << "IMAGE_FILE_HEADER: " << sizeof(IMAGE_FILE_HEADER) << endl;
cout << "IMAGE_OPTIONAL_HEADER32: " << sizeof(IMAGE_OPTIONAL_HEADER32) << endl;
cout << "IMAGE_SECTION_HEADER: " << sizeof(IMAGE_SECTION_HEADER) << endl;
getchar();
return 0;
}