1 什么是PE文件系统

​ PE文件是Windows操作系统下使用的可执行文件格式。 它是微软在UNIX平台的COFF(Common Object File Format, 通用对象文件格式)基础上制作而成的。 最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列的操作系统下

​ PE文件是指32位的可执行文件, 也称为PE32 。64位的可执行文件称为PE+或PE32+, 是PE (PE32)文件的一种扩展形式。PE文件种类如下:

2 PE文件的基本结构

PE文件结构如图:

2.1 DOS头 & DOS存根

​ 微软创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑了PE文件对DOS文件的兼容性。所以在PE头的最前面添加了一个IMAGE_DOS_HEADE结构体,用来扩展已有的DOSEXE头,DOS头和存根一共256字节

typedef struct _IMAGE_DOS_HEADER
{
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

IMAGE_DOS_HEADER结构体的大小为64个字节。在该结构体中必须知道2个重要成员:

  • e_magic:DOS签名(signature, 4D5A=>ASCIJ值”MZ”)
  • e_lfanew:指示NT头的偏移(根据不同文件拥有可变值)

​ DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(一般为192字节即使没有DOS存根,文件也能正常运行)。DOS存根由代码与数据混合而成,代码非常简单,在画面中输出字符串Thisprogram cannot be run in DOS mode后就退出。

2.2 NT头

IMAGE_NT_HEADERS结构体由3个成员组成:

  • 签名(Signature):其值为50450000h("PE" 00)
  • 文件头(FileHeader)结构体,20字节
  • 可选头(OptionalHeader) 结构体,224字节
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                  //4个字节的PE标志
    IMAGE_FILE_HEADER FileHeader;          //文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;    //可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

2.2.1 NT文件头

文件头是表现文件大致属性的IMAGE_FILE _HEADEfil结构体,20字节

typedef     struct _IMAGE_FILE_HEADER
{                                                                    
    WORD        Machine;                // 运行平台                         
    WORD        NumberOfSections;       // 文件的区块数目                  
    DWORD       TimeDateStamp;          // 文件创建日期和时间                
    DWORD       PointerToSymbolTable;   // 指向符号表(主要用于调试)        
    DWORD       NumberOfSymbols;        // 符号表中符号个数(同上)             
    WORD        SizeOfOptionalHeader;   // IMAGE_OPTIONAL_HEADER32 结构大小 
    WORD        Characteristics;        // 文件属性                         
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_ FILE_ HEADERS结构体中有如下4种重要成员(若它们设置不正确, 将导致文件无
法正常运行),参考自MS官方文档

  • Machine:每个CPU都拥有唯一的Machine码, 兼容32位Intel x86芯片的Machine码为14C
  • NumberOfSections:前面提到过 , PE文件把代码、 数据、 资源等依据属性分类到各节区中存储。 NumberOfSections用来指出文件中存在的节区(section)数量。 该值一定要大于0, 且当定义量与实际节区不同时, 将发生运行错误。
  • SizeOfOptionalHeaderSizeOfOptionlHeader成员用来指出IMAGE_OPTIONAL_ HEADER32结构体的长度。IMAGE_ OPTIONAL HEADER32结构体由C语言编写而成, 故其大小已经确定。 但是Windows的PE装载器需要查看IMAGE_FILE_HEADERSizeOfOptionlHeader值,从而识别出IMAGE_OPTIONAL_ HEADER32结构体的大小。
  • Characteristics:该字段用于标识文件的属性, 文件是否是可运行的形态、 是否为DLL文件等信息, 是将以下信息按位或运算的结果。
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

PE32+格式的文件中使用的是IMAGE_OPTIONAL_HEADER64结构体, 而不是IMAGE_ OPTIONAL_ HEADER32结构体。 2个结构体的尺寸是不同的, 所以需要在SizeOfOptionalHeader成员中明确指出结构体的大小。

2.2.2 NT可选头

IMAGE_OPTIONAL_ HEADER32是PE头结构体中最大的,有224字节

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_OPTIONAL_HEADER
{
    WORD    Magic;                          // 标志字, ROM 映像(0107h),普通可执行文件(010Bh) 
    BYTE    MajorLinkerVersion;             // 链接程序的主版本号 
    BYTE    MinorLinkerVersion;             // 链接程序的次版本号 
    DWORD   SizeOfCode;                     // 所有含代码的节的总大小 
    DWORD   SizeOfInitializedData;          // 所有含已初始化数据的节的总大小 
    DWORD   SizeOfUninitializedData;        // 所有含未初始化数据的节的大小 
    DWORD   AddressOfEntryPoint;            // 程序执行入口RVA 
    DWORD   BaseOfCode;                     // 代码的区块的起始RVA 
    DWORD   BaseOfData;                     // 数据的区块的起始RVA 

    DWORD   ImageBase;                      // 程序的首选装载地址 
    DWORD   SectionAlignment;               // 内存中的区块的对齐大小 
    DWORD   FileAlignment;                  // 文件中的区块的对齐大小 
    WORD    MajorOperatingSystemVersion;    // 要求操作系统最低版本号的主版本号 
    WORD    MinorOperatingSystemVersion;    // 要求操作系统最低版本号的副版本号 
    WORD    MajorImageVersion;              // 可运行于操作系统的主版本号 
    WORD    MinorImageVersion;              // 可运行于操作系统的次版本号 
    WORD    MajorSubsystemVersion;          // 要求最低子系统版本的主版本号 
    WORD    MinorSubsystemVersion;          // 要求最低子系统版本的次版本号 
    DWORD   Win32VersionValue;              // 莫须有字段,不被病毒利用的话一般为0 
    DWORD   SizeOfImage;                    // 映像装入内存后的总尺寸 
    DWORD   SizeOfHeaders;                  // 所有头+ 区块表的尺寸大小 
    DWORD   CheckSum;                       // 映像的校检和 
    WORD    Subsystem;                      // 可执行文件期望的子系统 
    WORD    DllCharacteristics;             // DLL文件特征属性设置(参考《PE文件权威指南》3.5.3小节)
    DWORD   SizeOfStackReserve;             // 初始化时的栈大小 
    DWORD   SizeOfStackCommit;              // 初始化时实际提交的栈大小 
    DWORD   SizeOfHeapReserve;              // 初始化时保留的堆大小 
    DWORD   SizeOfHeapCommit;               // 初始化时实际提交的堆大小 
    DWORD   LoaderFlags;                    // 与调试有关,默认为0  
    DWORD   NumberOfRvaAndSizes;            // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16 
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
                                            // 数据目录表 IMAGE_NUMBEROF_DIRECTORY_ENTRIES  =16
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  • AddressOfEntryPoint:程序入口点地址(EP),指向程序最先执行的代码的地址
  • ImageBase: 文件被加载到内存中时文件的优先装入地址。执行PE文件时, PE装载器先创建进程, 再将文件载入内存, 然后把EIP寄存器的值设置ImageBase+ AddressOfEntryPoint
  • DataDirectory:是IMAGE_DATA_DIRECTORY结构体数组, 数组的每项都有被定义的值。下面列出了各数组项的作用
  //结构体数组
  DataDirectory[0]    //导出表地址和大小,最常用表
  DataDirectory[1]    //导入表格地址和大小,最常用表
  DataDirectory[2]    //资源表地址和大小
  DataDirectory[3]    //异常表地址和大小
  DataDirectory[4]    //证书表地址和大小
  DataDirectory[5]    //基址重定位表的地址和大小
  DataDirectory[6]    //调试信息的起始地址和大小
  DataDirectory[7]    //特定于体系结构的数据地址和大小
  DataDirectory[8]    //全局指针寄存器相对虚拟地址
  DataDirectory[9]    //线程本地存储(TLS)表的地址和大小
  DataDirectory[10]    //加载配置表地址和大小
  DataDirectory[11]    //绑定导入表的地址和大小
  DataDirectory[12]    //导入地址表地址和大小
  DataDirectory[13]    //延迟导入描述符地址和大小
  DataDirectory[14]    //CLR头地址和大小
  DataDirectory[15]    //保留的目录

2.2.3 数据目录项

NT可选头最后一个成员为数据目录项(DataDirectory),该字段定义了 PE 文件中出现的所有不同类型的数据的目录信息。应用程序中的数据被按照用途分成很多种类, 如导出表、导入表、资源、重定位表等。

在内存中, 这些数据被操作系统以页为单位组织起来, 并赋以不同的访问属性:在文件中, 这些数据也同样被组织起来, 按照不同类别分别存放在文件的指定位置。该结构就是用来描述这些不同类别的数据在文件(和内存)中的位置及大小的。

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;    //数据起始RAV
  DWORD Size;            //数据块长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

​ 如果想在PE 文件中寻找特定类型的数据, 就需要从该结构开始。比如,要想查看PE 中都调用了哪些动态链接库的函数, 则需要从数据目录表的第2 个元素(数组编号为I)的IMAGE_DATA_DIRECTORY结构获取导人表在文件中的起始位置和大小, 然后再根据VirtuaLAddress_ I地址指向的位置找到导入表相关的字节码。

2.3 节区头

​ PE文件中的code (代码)、 data (数据)、 resource (资源)等按照属性分类存储在不同节区, 之所以这样做是因为这样可以保证程序的安全性。若把codedata放在一个节区中很容易引发安全问题。假如向字符串data写数据时,由于某个原因导致溢出(输入超过缓冲区大小时),那么其下的 code (指令)就会被覆盖,应用程序就会崩溃。 因此, PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为 "节区” 的地方, 然后需要把各节区属性记录在节区头中(节区 属性中有文件/内存的起始位置、 大小、访问权限等)。每个节区40字节

#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];    // 8个字节的节区名称
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;                    //加载到内存中的节区总大小
  } Misc;
  DWORD VirtualAddress;                    // 节区的 RVA 地址
  DWORD SizeOfRawData;                    // 磁盘上初始化数据的大小
  DWORD PointerToRawData;                // 在文件中节区的起始位置
  DWORD PointerToRelocations;            // 在OBJ文件中使用,重定位的偏移
  DWORD PointerToLinenumbers;            // 行号表的偏移(供调试使用地)
  WORD  NumberOfRelocations;            // 在OBJ文件中使用,重定位项数目
  WORD  NumberOfLinenumbers;            // 行号表中行号的数目
  DWORD Characteristics;                // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

2.4 PE文件的内存映射

PE文件加载到内存时, 每个节区都要能准确完成 文件偏移间内存地址之间的映射。 这种映射一般称为RAW TO RVA,如下图所示

内存映射中的几个概念

  • ImageBase:PE文件加载到内存中的起始地址
  • VA:虚拟内存地址
  • RVA:相对虚拟内存地址
  • FOA:文件偏移地址,在文件内的偏移地址
  • FileAlignment:在IMAGE_OPTIONAL_ HEADER32中,PE文件中区块对齐大小,为200h
  • SectionAlignment:在IMAGE_OPTIONAL_ HEADER32中,虚拟内存中区块对齐大小,为1000h

RVA to RAW 的一般流程

  • 判断相应的RVA所在节区(section)
  • 找到该节的起始RVA,SRVA = IMAGE_SECTION_HEADER.VirtualAddress
  • 求出节内偏移量Off,Off = SRVA – RVA
  • 找到该节的起始FOA,SFOA = IMAGE_SECTION_HEADER.PointerToRawData
  • 计算FOA,FOA = SFOA + Off

例如,在”.text”节区有文件偏移地址 FileOff

  • 算出实际 Off = FileOff - IMAGE_SECTION_HEADER.PointerToRawData
  • 计算RVA = IMAGE_SECTION_HEADER.VirtualAddress + Off
  • 之后可以根据ImageBase算出在内存中的地址

​ 讲解PE文件时经常出现 映像(Image)这一术语,PE文件加载到内存时, 文件不会原封不动地加载, 而要根据节区头中定义的节区起始地址、 节区大小等加载。 因此, 磁盘文件中的PE与内存中的PE具有不同形态。 将装载到内存中的形态称为映像以示区别, 使用这一术语能够很好地区分二者。

2.5 节区

程序中常见节区名和作用

  • .text:包含了CPU执行指令,一般来说, 这是唯一可以执行的节, 也应该是唯一包含代码的节
  • .rdataread only data,包含导入与导出函数信息,与Dependency Walker工具获得的信息相同;还可以存储程序所使用的其他只读数据,有些文件中还会包含.idata(import data)和.edata(export data)节, 来存储导入导出信息(见下表)。
  • .data:包含了程序的全局数据,可以从程序的任何地方访问到。
  • .rsrc:包含可执行文件所使用的资源,而这些内容并不是可执行的,比如图标、图片、菜单项和字符串等。

Dependency Walker:包含在微软Visual Studio的一些版本与其他微软开发包中,支待列出可执行文件的动态链接函数。下载地址:http://www.dependencywalker.com/

3 64位PE文件系统

为了保持向下兼容性,PE32+在原32位PE文件( PE32)的基础上扩展而来,PE32+使用IMAGE_NT_HEADER64结构体, 而PE32使用的是IMAGE_NT_HEADER32。这2种结构体的区别在于第三个成员, 前者为IMAGE_OPTIONAL_HEADER64 , 后者为IMAGE_OPTIONAL_HEADER32

typedef struct _IMAGE_NT_HEADERS64 {
 DWORD                   Signature;
 IMAGE_FILE_HEADER       FileHeader;
 IMAGE_OPTIONAL_HEADER64 OptionalHeader; //64位可选头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

文件头

PE32+中IMAGE_FILE_HEADER结构体的Machine字段值发生变化。

  • PE32文件中该Machine的值固定为0x014C
  • PE32+文件的Machine值为0x8664

可选头

PE32+ 中变化最大的部分就是IMAGE_OPTIONAL_HEADER结构体。

  • Magic字段值发生了改变,PE32中Magic值为0x010b, PE32+中Magic值为0x020b
  • BaseOfData:PE32文件中该字段用于指示数据节的起始地址(RVA), 而PE32+文件中删除了该字段。
  • ImageBase 字段的数据类型由原来的双字(DWORD)变为ULONGLONG类型( 8个字节)。这是为了适应增大的进程虚拟内存。
  • 栈和堆相关的字段(SizeOftackReserveSizeOfStackCommitSizeOfHeapReserve SizeOfHeapCommt)的数据类型变为ULONGLONG 类型(8个字节)。这样做也是为了与增大的 进程虚拟内存相适应。

AddressOfEntryPointSizeOfhnage 等字段大小与原PE32位是一样的,都是DWORD大小(4个字节, 32位)。这些字段的数据类型都是DWORD意味着PE32+格式的文件占用的实际虚拟内存中, 各映像的大小最大为4GB (32位)。但是由于ImageBase的大小为8个宇节(64位), 程序文件可以加栽到进程虚拟内存中的任意地址位。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注