MS-PE文件全解

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值