上篇讲到的PE文件DOS头中的两个重要成员,一个是e_magic 用来判段是否为PE文件,另一个 e_lfanew用来找出NT头的位置。
在DOS头与NT头 之间还有一部分区域,这里储存着一些被DOS头使用的数据,包括一些字符串等等,这部分的大小不太确定,所以,NT头的具体位置要由DOS头的最后一个成员来确定 。 我们可以利用这一部分空间实现一些特殊用途。
NT头的定义
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中第一个成员我们已经使用过了 可以用来判断是否为PE文件,在系统中定义为
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
另外两个成员都是结构体,里面存储的信息非常有用,解析这两个结构体才可以说是解析PE文件的开始。
NT头的第二个成员是文件头
这个文件头储存着关于这个PE文件的信息
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine: 标识这个文件可以运行在哪一个运行平台,并没有什么卵用,
NumberOfSections: 是区段的个数,也就是PE文件的主体被分为多少个部分
TimeDateStamp :表示文件是何时被创建的
PointerToSymbolTable: 符号表偏移,据说已经没什么用0.0
NumberOfSymbols: 符号个数,也没什么用
SizeOfOptionalHeader:扩展头的大小,32位和64位是不一样的
Characteristics:PE文件属性值,这个属性很重要,具体的值可以查询
如果在上篇文章中的代码里加上
PIMAGE_FILE_HEADER pFile = &pNt->FileHeader;
就可以遍历文件头的信息了。想要显示什么直接用pFile指出来就行。
NT头的第三个成员扩展头
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;
Magic:表示可选头的类型。
MajorLinkerVersion和MinorLinkerVersion:链接器的版本号。
SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
SizeOfInitializedData:初始化的数据长度。
SizeOfUninitializedData:未初始化的数据长度。
AddressOfEntryPoint:程序入口的RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。
BaseOfCode:代码段起始地址的RVA。
BaseOfData:数据段起始地址的RVA。
ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。
Win32VersionValue:保留,必须为0。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
Subsystem:运行该PE文件所需的子系统
DllCharacteristics:DLL的文件属性
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
LoaderFlags:保留,必须为0。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
DataDirectory:数据目录(非常重要)
下面是数据目录的定义
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress:是一个RVA。
Size:是一个大小。
这两个数一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。这个区域包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,这个等后面再讲。
我们可以在上篇的代码中加入
PIMAGE_OPTIONAL_HEADER pOptional = &pNt->OptionalHeader;
以此来解析NT头的扩展头,想要显示什么直接用pOptional指出来就行。