1.1 PE文件综述
PE,即Portable Executable的首字母简写,它是Win32平台可执行文件的标准格式。PE是微软(Microsoft)公司首先提出并着力推广的一种致力于可移植的可执行文件格式,可惜,实际上PE文件只限于Win32平台,并不能在苹果机或者其他工作站上运行。PE文件作为Win32平台可执行文件的标准格式提供了向上兼容的功能,即PE文件格式保留了MS-DOS中MZ头部。在Win32平台中,常见的PE文件有*.EXE、*.DLL、*.OCX等。
1.2 PE文件层次结构
PE文件头部首先第一部分是DOS头,DOS头部有两个部分构成,第一部分是MZ文件头,紧跟MZ文件头后面的是一个DOS可执行文件。正是由于DOS头的存在,使得所有PE文件向上兼容,使得所有PE文件都是合法的MS-DOS可执行文件。PE文件第二个部分是PE文件头,PE文件头有三个组成部分:PE文件标志、映像文件头、可选映像头。其中可选映像头中包含了数据目录表。这四个部分的具体介绍请阅读本文其后部分。PE文件的第三个部分是节表,节表与节是一一对应的,提供了每个节具体信息,可以认为节表是节的索引。PE文件的第四个部分是节,节中存在着文件真正的内容。在PE头的最后是调试信息,顾名思义,调试信息是用以调试的一些信息的汇总。
1.3 DOS头介绍
如上述综述所示,PE文件格式的第一个组成部分是DOS头。DOS头有两个部分组成:MZ文件头和DOS插桩(DOS Stub)程序。其实MZ文件头并不是什么新东西,在MS-DOS中就已经存在MZ文件格式了,之所以PE文件头中包含MZ文件头,是微软考虑到Windows系统与MS-DOS系统可持续文件的兼容性。MZ文件头占据了PE文件头64字节,该头定义在WINNT.h文件中:
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic;
USHORT e_cblp;
USHORT e_cp;
USHORT e_crlc;
USHORT e_cparhdr;
USHORT e_minalloc;
USHORT e_maxalloc;
USHORT e_ss;
USHORT e_sp;
USHORT e_csum;
USHORT e_ip;
USHORT e_cs;
USHORT e_lfarlc;
USHORT e_ovno;
USHORT e_res[4];
USHORT e_oemid;
USHORT e_oeminfo;
USHORT e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中较为常用的几个域是:e_magic和e_lfanew。e_magic是MZ头部的标志位,应为常量0x4D5A,即MZ的ASCII码。MZ是MZ文件头设计者名字首字母的缩写。e_lfanew指向PE头文件的位置,这样就可以跳过DOS插桩程序而直接定位到PE文件头。
DOS插桩程序是一个在MS-DOS中可执行的代码,用以代替原来MS-DOS中MZ文件的主体,在PE文件中,一般用之显示简单语句,可以认为其并无实际作用。
1.4 PE文件头
PE文件头是PE文件格式各部分中信息以及结构较为复杂的一个部分。与MZ头一样,PE文件头也定义在WINNT.h文件中,具体如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
从上面的定义中不难看出,PE文件头包含三个部分,PE文件标志(Signature)、映像文件头(FileHeader)以及可选映像头(OptionalHeader)。PE文件标志是一个常量,即“PE00”,它标志着PE文件头的开始,同时它也是PE文件的一个主要标识。我们可以通过DOS头中e_lfanew找出该标志的位置。
第二部分是映像文件头,它紧跟在PE文件标识的后面,映像文件头是一个结构体,它也被定义在WINNT.h文件中:
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代表着改程序要执行在的计算机的类型,X86的值为14CH;NumberOfSections表明了该PE文件的节数,该值与表的数量以及节表的数量应保持一致;TimeDateStamp表明了该文件生成的时间,可以用该值区分同一文件的不同版本;PointerToSymbolTable表明了COFF符号表的偏移;NumberOfSymbols表明了符号数目;SizeOfOptionalHeader表明了可选映像头的大小;Characteristics是标记,其中大部分的位用于OBJ文件或者LIB文件。
PE文件头的第三个部分是可选映像头,尽管名字是可选映像头,但是事实上它并不是可选而是必须存在的,是“必选”的。可选映像头也定义在WINNT.h之中:
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;
在可选映像中,将成员分为两个部分,前一部分称为标准域;后一部分称为附加域。所谓标准域,就是和UNIX可执行文件的COFF格式所公共的部分。虽然标准域保留了COFF中定义的名字,但是Windows NT仍然将它们用作了不同的目的——尽管换个名字更好一些。 在标准域中,第一个成员变量是所谓的幻数,一般来说是0x010B;下面两个值是链接器的主、次版本号,但是这两个值并不可靠;第四、五、六值是可执行代码的长度,初始化数据的长度,以及未初始化数据的长度。第七个值是RVA,所谓RVA是相对虚拟地址,相对虚拟地址是一个相对于PE文件映射到内存的基地址的偏移量。AddressOfEntryPoint指示了程序开始执行的地方。若要改变整个执行的流程,可以通过改变AddressOfEntryPoint的值实现。这个值是计算机病毒非常关注的一个值,一般来说病毒改变该值使得病毒代码首先被执行,在病毒代码的最后使用跳转至原来正常程序入口处,这样就可以将病毒代码嵌入到正常程序之中。标准域中最后两个值指示了可执行代码起始位置以及初始化数据起始位置,但这两个值意义不大,因为可以通过后面的节获得更为可靠的信息。
除去以上所说的几个值之外,结构体中剩下的值都属于附加域。在附加域中,并不是所有的值计算机病毒都关心的,所以了解附加域中所有值是徒劳且没有必要的。下面介绍附加域中计算机病毒比较关心和比较重要的几个值。ImageBase,它是PE文件的首选装载地址。一般来说,ImageBase默认是400000H,但不是必须的,可以人工该做其他值。以ImageBase为400000H为例,它代表着理想情况下,程序被默认加载到400000H处,但是如果该处已经被其他模块占据,那么则将该程序加载到其他空闲处。SectionAlignment,它是可执行文件装入内存时的对齐粒度,SectionAlignment默认是1000H,但不是必须的,可以人工该做其他值。以SectionAlignment默认值1000H为例,所谓对齐粒度是指,如果可执行文件装入内存时占用非1000H整数倍的空间,那么,则自动填入无效的信息,使其长度变成1000H的整数倍。尽管这样降低了内存的使用率,但是却提高了内存读写效率。在内存容量不断增大的今年,时间复杂性已经超越空间复杂性成为计算机工作者首要关心的指标,从这个角度看,就不难明白为什么会牺牲内存利用率来提高内存读写效率。我们将填入的无效信息成为程序的空洞。现在计算机病毒正在日益关注程序的空洞,因为这些空洞使得可以在不改变程序大小的情况下,在正常程序中嵌入病毒程序。FileAlignment,它同SectionAlignment是一对相近的概念,它是文件在磁盘上节的对齐粒度,一般它同扇区大小是一致的,即200H。SizeOfImage,它是内存中PE文件映像体的尺寸,由于在内存中采取了以SectionAlignment为粒度的对齐,所以,实际上它并不是PE文件的实际大小,而是经过对齐之后PE文件占据内存大小。SizeOfHeaders,它表示整个头部的大小,实际值等于文件大小减去所有节的大小,可以以此值作为PE文件第一个节的文件偏移量。DataDirectory,它是一个IMAGE_DATA_DIRECTORY结构数组,每个结构给出了一个重要数据结构的RVA。
1.5 节表
节表是紧挨着NT映像头的一结构数组,它的数量与节的数量是一致的,也与映像头文件中NumberOFSections是一致的。节表也定义在WINNT.h文件中,具体如下:
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,它是节的名字。PhysicalAddress,OBJ文件用来表示本节的物理地址。VirtualSize,EXE文件中表示节的实际字节数。VirtualAddress,它是本节的RVA,RVA在文件前面已介绍。通过它可以得到该节在内存中的物理地址,如果ImageBase取默认值400000H,而VirtualAddress取值1000H,那么在内存中,本节的地址是401000H。SizeOfRawData,经过对齐之后节占据内存的大小,它和SizeOfImage取值原理是一致的。PointerToRawData节基于文件的偏移量,计算机病毒在修改文件时,要给出该值。PointerToRelocations,OBJ中表示该节重定位的信息,EXE中没有意义。PointerToLinenumbers,行号编号。NumberOfRelocations,本节中需要重定位的数目。NumberOfLinenumbers,本节中在行号表中的行号数目。Characteristics,节属性,其意义如下表所示:
表二:节的属性
值 | 意 义 |
8 | 保留 |
20H | 包含代码 |
40H | 包含已初始化的数据 |
80H | 包含未初始化的数据 |
100H | 连接器使用,保留 |
200H | 连接器使用,保存有注释或其他链接器使用的数据 |
800H | 链接器使用 |
1000H | 链接器使用 |
100000H | 1字节对齐 |
200000H | 2字节对齐 |
300000H | 4字节对齐 |
400000H | 8字节对齐 |
500000H | 16字节对齐 |
600000H | 32字节对齐 |
700000H | 64字节对齐 |
1000000H | 包含扩展的重定位数据 |
2000000H | 节可以被丢弃 |
3000000H | 不使用CACHE |
8000000H | 不分页的 |
10000000H | 分享的 |
20000000H | 可执行的 |
40000000H | 可读的 |
80000000H | 可写的 |