Last updated on October 13, 2023 am
                  
                
              
            
            
              
                
                 PE文件结构(一)DOS头 NT头
 
总起 首先我们需要知道PE是什么。PE,即Portable Executable,也就是Windows下的可执行文件,它包括exe文件,也包括dll、sys文件等,当我们使用十六进制文本编辑器打开一些exe文件时,会得到如下样子
如图是百度网盘的前面一小撮的部分
当我们打开其他的exe文件时,这一小撮内容应当是差不多的。这其实就说明这些PE文件应当具有某种格式。我们不妨先看看整个PE文件结构图
当我们只看最上面一行,可以发现PE文件大致分为如下几部分
IMGAE_DOS_HEADR - DOS头 
DOS STUB - DOS填充 
IMAGE_NT_HEADR - NT头 
SECTION_TABLE - 节表 
SECTIONS - 节/节区 
OTHERS 
 
在上面给出的百度网盘的文件格式中,所展现的就是DOS头+DOS填充部分。可能你会有个问题,即
这些部分有什么含义? 
哪个部分在哪里开始哪里结束呢? 
 
不急 ,这里先给出整个PE文件结构的图
可以看到在上面那个图中,又引申出许多小表,其实这些小表就是一个一个结构体,存储在PE文件结构中。可以这么说:PE文件结构就是由许许多多的结构体组成,每个结构体里的每个成员都有相应的含义,比如指示哪个部分的开始,哪个部分的结束,在指来指去的过程中,整个文件运行所需要的信息也就明确了。
DOS头 我们先来看看DOS头部分的结构体吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct  _IMAGE_DOS_HEADER {     0X00  WORD e_magic;                                                                                                0x3C  DWORD e_lfanew;     };
 
简单算算,即整个DOS头只有0x40个字节,那就对比这个结构体来分析一下百度网盘的DOS头吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct  _IMAGE_DOS_HEADER {     0X00  WORD e_magic;                                                                                                0x3C  DWORD e_lfanew;     };
 
这里只需要注意第一个成员以及最后一个成员,第一个成员 e_magic 是可执行文件标志,在一个程序运行时,会首先检查这两位来判断这是不是一个可执行文件,最后一个成员e_lfanew就是存储的就是NT头的地址
可以看到e_lfanew有一个箭头指向了IMAGE_NT_HEADERS,也就是NT头。
NT头 先根据DOS头中e_lfanew所指示的地址0x150,在16进制文本编辑器看看这个NT头是个啥吧
根据上面的PE文件结构图,NT头又可以分成三个部分
Signature 
Image_File_Header - 标准PE头 
Image_Optional_Header - 可选PE头 
 
其中Signature占4个字节,即 50 45 00 00  在右边也可以看到就是PE两个字母的ascii码(50 45)
标准PE头 标准PE头占0x14个字节,它也是个结构体,如下是他的结构体成员及相关含义,带星号为重要成员
1 2 3 4 5 6 7 8 9 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;
 
可选PE头 标准PE头过后就是可选PE头,这里就仅给出结构体成员,带星号为重要成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 typedef  struct  _IMAGE_OPTIONAL_HEADER  {        WORD    Magic;       BYTE    MajorLinkerVersion;       BYTE    MinorLinkerVersion;       DWORD   SizeOfCode;       DWORD   SizeOfInitializedData;      DWORD   SizeOfUninitializedData;      DWORD   AddressOfEntryPoint;      DWORD   BaseOfCode;      DWORD   BaseOfData;      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;  
 
RVA 这里提到了几个新的名词RVA,即Relative Virual Address,虚拟相对地址。
在解释此名词之前,需要明白一件事情,即PE文件实际上有两种格式,一种是存储在硬盘中,也就是16进制编辑器打开的样子,还有一种则是运行时,操作系统会分配一个独立的虚拟空间给它,通常这个虚拟空间大小是4GB,然后把这个文件装载到这个4GB中,装载到哪,则是先由ImageBase字段决定,一旦确定好从哪个地方装载,就相当于确定了一个基址,而这个RVA就是相对这个基址的偏移。
比如这个文件从内存中0x10000000开始装载,如果RVA是0x32,那么在内存中它的地址就是0x10000032
FOA 可能会想起,那我们刚刚提到的e_lfanew是什么偏移?,既然PE文件有两种格式,那么偏移肯定也有两种,刚刚的提及的RVA是在虚拟内存中的,那么还有一种就是在文件中的偏移,即FOA - File Offset Address,也就是在16进制文本编辑器里的偏移
SectionAlignment and FileAlignment SectionAlignment :在内存里面节的对齐大小,必须是这个的整数倍
FileAlignment:在文件里面节的对齐大小,必须是这个的整数倍
使用代码读取 得益于微软的内置库,可以方便的使用以及定义好的指针类型来读取,而不需要自己手动计算
唯一需要注意的是,由于是文件读入内存中,因此文件在内存中相当于是有一个基址的,所以需要buffer+FOA。注意这里是把文件原封不动的读入程序内存,而不是运行时拉伸进内存。
可以使用网上的一些PE查看器来验证自己是否读出正确数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <stdio.h>  #include <windows.h>  void  PE_info (FILE* fp,char * buffer) {     PIMAGE_DOS_HEADER DosHeader = NULL ;     PIMAGE_NT_HEADERS NTHeader = NULL ;     PIMAGE_FILE_HEADER FILEHeader = NULL ;     PIMAGE_OPTIONAL_HEADER OptionalHeader = NULL ;     DosHeader = (PIMAGE_DOS_HEADER)(buffer);     printf ("=================DosHeader PE_INFO=================\n" );     printf ("DOS_HEADER e_magic:%X\n" ,DosHeader->e_magic);     printf ("DOS_HEADER e_lfanew:%X\n" ,DosHeader->e_lfanew);          NTHeader = (PIMAGE_NT_HEADERS)(buffer + DosHeader->e_lfanew);     printf ("=================NTHeader PE_INFO=================\n" );     printf ("IMAGE_DOS_SIGNATURE:%X\n" ,*(PDWORD)NTHeader);     FILEHeader = (PIMAGE_FILE_HEADER)(buffer + DosHeader->e_lfanew + 4 );     printf ("=================FileHeader PE_INFO=================\n" );     printf ("Machine:%X\n" , FILEHeader->Machine);     printf ("Number of Sections:%X\n" , FILEHeader->NumberOfSections);     printf ("Size of OptionalHeader:%X\n" , FILEHeader->SizeOfOptionalHeader);     printf ("Time Stamp:%X\n" , FILEHeader->TimeDateStamp);     OptionalHeader = (PIMAGE_OPTIONAL_HEADER)(buffer + DosHeader->e_lfanew + 4  + IMAGE_SIZEOF_FILE_HEADER);     printf ("=================Optional PE_INFO=================\n" );     printf ("Magic:%X\n" , OptionalHeader->Magic);     printf ("Address of EntryPoint:%X\n" , OptionalHeader->AddressOfEntryPoint);     printf ("ImageBase:%X\n" , OptionalHeader->ImageBase);     printf ("SizeOfImage:%X\n" , OptionalHeader->SizeOfImage);     printf ("SizeOfHeaders:%X\n" , OptionalHeader->SizeOfHeaders); }int  Size (FILE* fp) {     int  size = 0 ;     fseek(fp,0 ,SEEK_END);     size = ftell(fp);     fseek(fp,0 ,SEEK_SET);     return  size; }int  main () {     char * buffer;     FILE* fp = NULL ;     errno_t  err_1 = fopen_s(&fp, "YOU PATH TO EXE HERE" , "rb" );               int  size = Size(fp);     buffer = (char *)malloc (size);     fread(buffer,size,1 ,fp);     PE_info(fp,buffer);     return  0 ; }
 
总结 DOS头中第一个成员e_magic和最后一个成员e_lfanew最重要,前者是可执行文件的标志MZ,后者是NT头的文件偏移
NT头分三个部分,分别是
Signature - PE
 
标准PE头,其重要成员
Machine 可以运行在什么样的CPU 
NumberofSections 节的数量 
TimeDateStamp 时间戳 
SizeOfOptionalHeader 可选PE头大小 
Characteristics 文件属性 
 
 
可选PE头,其重要成员且仍有用成分
Magic    该PE文件是什么类型的 10B 32位PE // 20B 64位PE // 107 ROM映像 
AddressOfEntryPoint 程序入口,即所谓的OEP 
ImageBase   程序的优先装载地址 
SectionAlignment  内存中节的对齐粒度 
FileAlignment  文件中节的对齐粒度 
SizeOfImage 映像大小 
SizeOfHeaders 所有节表对齐后的大小 
SizeOfStackCommit  初始化时实际提交的栈的大小 
SizeOfHeapReserve   初始化时实际提交的堆的大小 
NumberOfRvaAndSizes 目录项 
(无用但保留的字段基本都是为了兼容)