MS-PE文件全解

来源:互联网 发布:mac剪切 编辑:程序博客网 时间:2024/06/06 21:40

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,构成一个循环,就可以次将所有的节区信息都打印出来,如下图:



原创粉丝点击