MS-PE文件全解(一)
MS-PE文件时微软的Windows系列NT操作系统中的可执行文件的简称,它与传统的UNIX可执行文件ELF相比,稍微复杂一点,结构内容也多了一些。对于编写如杀毒软件,加密可执行程序的软件(加壳软件)等,熟悉它还是非常有必要的。
好了,下面切入主题。
MS-PE文件,以下将简称PE文件。PE文件大体分为四个部分,DOS部分,PE头部分,节表,节区数据,最前面的是DOS的头部,结构如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在打开一个二进制的PE文件的数据文件时,首先要判断一下e_magic的标志是否为"ZM"(这是内存布局,正常来看是“MZ”),下面的都是DOS执行文件相关的数据,上面已经以英文方式注视出,结构体定义取自微软的SDK的头文件中,最后一项对PE文件有意义是e_lfanew,它指出这个文件从文件的开头偏移到哪个位置,继续后面的后续数据,比如说,如果是0x3C,就是指从文件开头偏移到0x3C再继续。如下图的偏移到0xC8位置:
到了指定的偏移位置时,还需要进一步检查标志,以防止打开的文件是有效的PE文件,首先要检查的是PE标志是否为“EP”(这是内存布局,正常来看是“PE\0\0”),
如图:
到达的这个位置,其为一个数据结构体,通常称为NtHeaders,如下:
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
NtHeaders的Signature即为“PE\0\0”,FileHeader是一个结构体,OptionalHeader还是一个结构体。其意思与他的字面意思相似,不过Optional不可认为可选,它旨在说明其有可拓展性,而且重要性更高。
这里这个NtHeaders有32位和64位之分,判断是32位还是64,需要判断两个标识,这里先给出第一个结构体:
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就是文件的标识,标识可运行的哪种CPU类型下,如0x14C表示32位的环境,而0x8664表示64位的环境,NumberOfSections指的是后面的节区数目(将在后面介绍),TimeDateStamp是个文件的时间戳,记录了文件被链接器生成后写入的时间戳,这个时间戳需要用C语言的库函数mktime和gmtime来完成解码,得到可以可以阅读的时间数据,PointerToSymbolTable是一个链接符号表,与NumberOfSymbols一样用于目标文件之用,分别指定符号表和符号数目,SizeOfOptionalHeader是紧随其后的IMAGE_OPTIONAL_HEADER的大小,而Characteristics指定文件的属性。
从NtHeaders中就可看出OptionalHeader紧随其后,这是IMAGE_OPTIONAL_HEADER的结构体,有两个版本,分别是64位和32位版本,其实整个NtHeader的64位和32位的区别就在此处。
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;
上面这个32位版本的,下面这个是64位版本的,,
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG 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;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
其实仔细看一下,两个版本其实大同小异,只是64位版本中有的数据位不是原来的32位了,拓展到了64位,并且在64位中省去BaseOfData这个成员。
数据结构体中,Magic也是标志运行平台的标志,通常32位是0x10B,而在64位平台为0x20B,ROM则是0x107,所以这个也可用于判断64位还是32位,最好结合上面的Machine,两个依次来判断,MajorLinkerVersion和MinorLinkerVersion表示链接器的版本号,SizeOfCode是所有的代码的字节数,这个不能出错,否则运行时,会出现不是一个合法的windows程序的出错信息,SizeOfInitializedData和SizeOfUninitializedData指定初始化数据大小和非初始数据大小,很多都能从字面来理解,AddressOfEntryPoint是一个可执行文件的入口地址,这个地址通常是个RVA(相关联的虚拟地址),若要得到文件中的偏移,还需在节区中找对应值范围来确定。BasOfCode是个RVA,用以指定代码段的在加载到处内存后,其实地址的值,同样BaseOfData亦是如此。ImageBase是整个可执行文件加载到内存后,可执行文件的镜像起始地址,这个是必须是64k的整数倍,SectionAlignment是指定节区的对齐大小,也就书当文件运行时加载到内存后,后面的节区按这个数的整数倍对齐,而FileAlignment就是在磁盘上的后面节区对齐大小。
后面的这几个,可以从字面去理解,有的甚至没有任何意义,如Win32VersionValue,SizeOfImage的数据是这个镜像的大小,而且按照SectionAlignment对齐,不可出错,否则运行的加载时就会报错,SizeOfHeaders是所有的头的大小,包括DOS的头,PE的头,和还未介绍的Section头,同样大小不可错,它的大小是按照FileAlignment的对齐的。
CheckSum通常在DLL中用的广泛,EXE中通常置零,它是这个文件的CheckSum的值,用于文件校验,防止文件损坏等,Subsystem是子系统的含义,常见的子系统有Windows GUI,Windows CUI,Native等,如图:
SizeOfStack***和SizeOfHeap***分别用于初始时,堆和栈的分配的大小,LoaderFlags也是一个没有意义的数据,
NumberOfRvaAndSizes指定后面的DataDirectory的入口数量,IMAGE_DATA_DIRECTORY结构的定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这样的结构通常有16个,分别表示Export table ; import table ; resource table ; exception table ; certificate table ; base relocation table ; debug ; architecture ; global ptr ; TLS table ; Load Config Table ; Bond Import ; IAT ; Delay Import Descriptor ; CLR Runtime Header ; (reserved,未使用),这15个表分别指定一个VirtualAddress,他是个RVA,以及其大小。
在下来就是节区表头,节区表头的数目在上面的FileHeader的NumberOfSections中提到过,它们按照数目一个接一个地排在一起的,如图:
节区表的结构定义如下:
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;
Name指定节区的名称,它是8个字符长的字串,Misc.VirtualSize就是节区按照SectionAlignment对齐加载到内存后的节区大小,VirtualAddress是一个RVA,是相对虚拟地址,SizeOfRawData才是真实的在磁盘上的节区大小,其按FileAlignment对齐,PointerToRawData就是磁盘文件的偏移位置,人们通常利用VirtualAddress和SizeOfRawData和PointerToRawData来将RVA转成磁盘文件偏移。
在编程时,通过结合上面的NumberOfSections,构成一个循环,就可以次将所有的节区信息都打印出来,如下图: