本文最后更新于:星期二, 六月 16日 2020, 3:16 下午

以test.exe文件为例子,分析PE文件的各部分格式

1. MZ文件头

用winhex打开test.exe,找到MZ文件头,为偏移0x00~0x3F的部分

1568533651252

其中每个字段的意义如下所示:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number设置ox5A4D  ascii码值为'MZ'(标志,不会变的标志)
    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;                    //PE头文件的偏移地址
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

比较重要的有以下几个字段:

  • offset 0x00-0x01:e_magic字段,一般为4D5A,表示这是一个PE文件
  • offset 0x3C-0x3D:e_lfanew字段,表示PE文件头的偏移地址,根据这个可以找到PE文件头,在这里为0x000000B0

2. DOS stub

接下来的偏移0x40-0xAF部分,处于MZ头部和PE文件头之间,称为DOS stub,它与MZ头部一起组成DOS小程序

1568534266390

可以看到其中一部分的ASCII码显示为“This is program connot be run in DOS mode.”,这一部分的长度不确定。

3. PE文件头

从MZ文件头中指示的偏移0x000000B0开始为PE文件头,其中包含三个部分:

typedef struct _IMAGE_NT_HEADER
{
    DWORD  Signature;                             //PE Signature : 50450000("PE"00)
    IMAGE_FILE_HEADER  FileHeader;                //文件头结构体
    IMAGE_OPTIONAL_HEADER32  OptionalHeader;      //可选头结构体
} IMAGE_NT_HEADER, *PIMAGE_NT_HEADER32;

① PE Signature(签名)

这一字段占0x004字节,为固定子字串”PE \ 0 \ 0”,即50450000,标志着PE文件头的开始

1568534846590

② FileHeader(映像文件头)

紧跟在PE签名后的是映像文件头,占0x014字节,各字段的意义如下:

typedef struct _IMAGE_FILE_HEADER
{
    WORD  Machine;                         //每个CPU都拥有唯一的machine码
    WORD  NumberOfSections;                //节区数量,当定义节区数与实际不同时会发生错误
    DWORD  TimeDateStamp;
    DWORD  PointerToSymbolTable;
    DWORD  NumberOfSymbols;
    WORD  SizeOfOptionalHeader;            //IMAGE_OPTIONAL_HEADER32结构体的大小,固定的
    WORD  Characteristics;                 //文件属性,0x0002h为可执行文件,0x2000h为DLL文件
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEAD

test.exe中的实际情况如下:

1568535401922

其中比较重要的几个字段:

  • offset 0xB6-0xB7:该字段为NumberOfSections,定义了节区的数量,在这里的值为0x0003,说明该程序含有三个节区
  • offset 0xC4-0xC5:该字段为SizeOfOptionalHeader,定义了可选映像头(OptionalHeader)的大小,在这里的值为0x00E0

③ OptionalHeader(可选映像头)

根据映像文件头中的信息,可选映像头占0xE0字节,其中各字段意义如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
WORD    Magic;            //标志字(32位时0x10Bh)
BYTE    MajorLinkerVersion;        //连接器主版本号
BYTE    MinorLinkerVersion;        //连接器次版本号
DWORD   SizeOfCode;            //代码段大小
DWORD   SizeOfInitializedData;    //已初始化数据块大小
DWORD   SizeOfUninitializedData;    //未初始化数据块大小
DWORD   AddressOfEntryPoint;     //EP的RVA值,程序最先执行代码的地址
DWORD   BaseOfCode;            //代码段起始RVA
DWORD   BaseOfData;            //数据段起始RVA
DWORD   ImageBase;            //PE文件的装载地址
DWORD   SectionAlignment;        //块对齐,节区在内存中最小单位
DWORD   FileAlignment;        //文件块对齐,节区在文件中的最小单位
WORD    MajorOperatingSystemVersion;//所需操作系统版本号
WORD    MinorOperatingSystemVersion;//
WORD    MajorImageVersion;        //用户自定义主版本号
WORD    MinorImageVersion;        //用户自定义次版本号
WORD    MajorSubsystemVersion;    //win32子系统版本。若PE文件是专门为Win32设计的
WORD    MinorSubsystemVersion;    //该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD   Win32VersionValue;        //保留
DWORD   SizeOfImage;            //内存中整个PE映像体的尺寸
DWORD   SizeOfHeaders;        //所有头+节表的大小,即整个PE头的大小
DWORD   CheckSum;            //校验和
WORD    Subsystem;            //NT用来识别PE文件属于哪个子系统(系统驱动、GUI、CUI)
WORD    DllCharacteristics;        
DWORD   SizeOfStackReserve;        
DWORD   SizeOfStackCommit;        
DWORD   SizeOfHeapReserve;        
DWORD   SizeOfHeapCommit;        
DWORD   LoaderFlags;            
DWORD   NumberOfRvaAndSizes;    //指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

test.exe中的实际情况如下:

1568537091093

其中比较重要的几个字段:

  • offset 0xD8-0xDB:该字段为AddressOfEntryPoint,定义了文件开始执行位置的相对虚拟地址(RVA),在这里的值为0x00001000
  • offset 0xE4-0xE7:该字段为ImageBase,定义了可执行文件默认装入的内存地址,在这里的值为0x00400000

根据以上两个值,我们可以计算出程序入口的虚拟地址(VA)为0x00401000

除此之外,还有两个特别重要的值:

  • offset 0x124-0x127:该字段为NumberOfRvaAndSizes,指示了数据目录项(DataDirectory)的项数,在这里的值为0x00000010,即16项
  • offset 0x128-0x1A7:该字段为DataDirectory,是一个IMAGE_DATA_DIRECTORY数组,里面存放的是可执行文件的一些重要部分的起始RVA和尺寸,目的是使可执行文件更快地进行装载

IMAGE_DATA_DIRECTORY的结构如下:

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

数据表成员的结构如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory  
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory  
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory  
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory  
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory  
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table  
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory  
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)  
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    8   // Architecture Specific Data  
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       9   // RVA of GP  
#define IMAGE_DIRECTORY_ENTRY_TLS             10   // TLS Directory  
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    11   // Load Configuration Directory  
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   12   // Bound Import Directory in headers  
#define IMAGE_DIRECTORY_ENTRY_IAT            13   // Import Address Table  
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   14   // Delay Load Import Descriptors  
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 15   // COM Runtime descriptor  

分析数据,可以发现数据目录中只有两项:

  • IMAGE_DIRECTORY_ENTRY_IMPORT(Import Directory)RVA=0x00002014 Size=0x0000003C
  • IMAGE_DIRECTORY_ENTRY_IAT(Import Address Table)RVA=0x00002000 Size=0x00000014

4. 节表(Section Table)

紧接着PE文件头的是节表。节表实际上是一个结构数组,每个结构包含了一个节的具体信息(每个结构占用0x28字节),该结构的内容如下:

#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;

test.exe 节表中结构的数目由之前映像文件头中的 NumberOfSections 字段确定,一共包含3个该结构,分别是 .text , .rdata.data ,具体如下:

1568538571454

① .text节表

其中比较重要的几个字段:

  • offset 0x1A8-0x1AF:该字段为Name,可以从ASCII码值看出该节表对应的节为.text节
  • offset 0x1B0-0x1B3:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x00000046字节
  • offset 0x1B4-0x1B7:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00001000
  • offset 0x1BC-0x1BF:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000400
  • offset 0x1CC-0x1CF:该字段为Characteristic,表示该节的属性,这里的值为0x60000020 = 0x40000000 + 0x20000000 + 0x20,表示的意义是该节包含代码,并且可读可执行

② .rdata节表

其中比较重要的几个字段:

  • offset 0x1D0-0x1D7:该字段为Name,可以从ASCII码值看出该节表对应的节为.rdata节
  • offset 0x1D8-0x1DB:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x000000A6字节
  • offset 0x1DC-0x1DF:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00002000
  • offset 0x1E4-0x1E7:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000600
  • offset 0x1F0-0x1F7:该字段为Characteristic,表示该节的属性,这里的值为0x40000040 = 0x40000000 + 0x40,表示的意义是该节包含已初始化的数据,并且可读

③ .data节表

  • offset 0x1F8-0x1FF:该字段为Name,可以从ASCII码值看出该节表对应的节为.data节
  • offset 0x200-0x203:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x0000008E字节
  • offset 0x204-0x207:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00003000
  • offset 0x20C-0x20F:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000800
  • offset 0x21C-0x21F:该字段为Characteristic,表示该节的属性,这里的值为0xC0000040 = 0x80000000 + 0x40000000 + 0x30,表示的意义是该节包含已初始化的数据,并且可读可写

5. 节(Section)

① .text节

这一节含有程序的可执行代码,根据节表中的值,可以确定.text节在文件中的地址为0x00000400,实际长度为0x46字节,具体代码如下:

1568558607717

② .rdata节

这一节称为引入函数节,包含有从其他DLL中引入的函数,根据IMAGE_DATA_DIRECTORY中引入函数表地址为0x00002014,且根据节表,该表在内存中的偏移为0x00002000,即RVA为0x0。根据节表,该节从文件偏移0x600处开始,那么.rdata表在文件中的偏移就为0x614。大小为0xA6字节。

该节的开始是一个成员为IMAGE_IMPORT_DESCRIPTOR结构的数组,该结构的定义如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
     union {
          DWORD Characteristics;         // 0 for terminating null import descriptor
          DWORD OriginalFirstThunk;   // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
     };
     DWORD TimeDateStamp;           // 0 if not bound,
                                    // -1 if bound, and real date\time stamp
                                    // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                    // O.W. date/time stamp of DLL bound to (Old BIND)
      DWORD ForwarderChain;           // -1 if no forwarders
      DWORD Name;                             // RVA,指向字符串,是这个可执行文件的名字。例如"ACE.dll"
      DWORD FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

该节的实际数据如下:

1568597519788

第一个IDD的分析如下:

  • offset 0x614-0x617:该字段为OriginalFirstThunk,是一个IMAGE_THUNK_DATA的指针,值为0x00002050,因为该节在内存中的偏移为0x00002000所以RVA为0x50,在文件中即为0x00000650,这个地址对应的即是第一个IID的IMAGE_THUNK_DATA,其结构如下
typedef struct _IMAGE_THUNK_DATA32 {
     union {
           DWORD ForwarderString;          // 一个RVA地址,指向forwarder string 
           DWORD Function;                 // PDWORD,被导入的函数的入口地址
           DWORD Ordinal;                  // 该函数的序数
           DWORD AddressOfData;            // 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
      } u1;
} IMAGE_THUNK_DATA32;

(IMAGE_THUNK_DATA64与IMAGE_THUNK_DATA32的区别,仅仅是把DWORD换成了64位整数。)

所以0x650的值在这里对应的字段为AddressOfData,即一个指向IMAGE_IMPORT_BY_NAME的指针,为0x00002064,即RVA为0x64,在文件中的偏移为0x664,该结构的定义如下:

typedef struct _IMAGE_IMPORT_BY_NAME {
     WORD Hint;            // 该函数的导出序数
     BYTE Name[1];      // 该函数的名字
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

前两个字节为引入函数的序号,其值为0x0080,后面紧跟的为函数名字符串”ExitProcess”,以0x00结尾

  • offset 0x620-0x623:该字段为Name,是指向字符串的指针,值为0x00002072,所以文件中偏移为0x00000672,对应的就是”kernel32.dll”,以0x00结尾
  • offset 0x624-0x627:该字段为FirstThunk,是指向字符串的指针,值为0x00002000,所以文件中偏移为0x00000600,该地址在内存中将会设置成API函数ExitProcess的真实地址

第二个IDD的分析如下:

  • offset 0x628-0x62B:该字段是OriginalFirstThunk,与第一个IDD的相同,值为0x00002058,RVA为0x58,文件中的偏移为0x658,指向的地址的值为0x0000208C,该地址是指向IMAGE_IMPORT_BY_NAME结构的指针,该值的RVA为0x8C,在文件中的偏移为0x68C,对应的前两个字节为引入函数序号0x019D,后面紧跟的为函数名字符串”MessageBox”,以0x00结尾
  • offset 0x634-0x637:该字段为Name,是指向字符串的指针,值为0x0000209A,所以文件中偏移为0x0000069A,对应的就是”user32.dll”,以0x00结尾
  • offset 0x638-0x63B:该字段为FirstThunk,是指向字符串的指针,值为0x00002008,所以文件中偏移为0x00000608,该地址在内存中将会设置成API函数MessageBox的真实地址

③ .data节

.data节称为已初始化的数据节,其中存放的是在编译时刻已经确定的数据。可以从节表中知道,该节从文件偏移0x800处开始,实际大小为0x8E字节,实际数据如下:

1568638958205

可以看到其中存放着一些程序预设好的字符串,这一部分在程序运行时也会被装入内存之中


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Docker学习笔记 上一篇
Flask学习笔记(一) 下一篇