pe文件结构只能说算一点点基础,顺带学习PE的时候再熟悉熟悉c语言和Windows API
PE文件即Portable Execute Windows下可执行文件的总称,常见的有 DLL,EXE,OCX,SYS 等,PE文件可以说是在各个领域都有涉及,特别是病毒领域,内网渗透中的免杀对抗
(免杀对抗环境从普通的杀软到edr,xdr等设备)
基础知识 PE文件的结构有两种表现形式:一是存储在在硬盘中的文件,二是加载在内存中
 
如上图可见,当PE文件加载到内存中后,DOS头到最后一个节区头的部分是一致的,而之后节区与节区之间的间隔,内存中的间隔会更大
产生差异是因为内存对齐,主要是下面两点:
操作系统通常以内存分页为单位(通常是 4 KB 或 2 MB 等)来管理内存。加载 PE 文件时,每个节会被分配到与分页边界对齐的内存地址 
在 PE 文件的头信息中,SectionAlignment 字段定义了各个节(段)在内存中的对齐方式。这种对齐方式一般也选择 4 KB 或更大,以保证节在内存中符合分页要求 
 
在开始之前,先来点基础概念。对于学过操作系统的来说,这应该很好理解
地址 
 
一般是指虚拟地址,而非物理地址,我们知道程序运行时候是使用操作系统分配的内存空间,所以用户并不知道具体的物理地址。
镜像文件 
 
包含以 EXE 文件为代表的 “可执行文件”、以DLL文件为代表的“动态链接库”。因为他们常常被直接“复制”到内存,有“镜像”的某种意思。
RAV 
 
Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。
一般来说,PE文件在硬盘上和在内存里是不完全一样的。各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些 “空洞”,这样占用的空间就会大一些 。
因为存在这种对齐,所以在 PE 结构内部,表示某个位置的地址采用了两种方式:
针对在硬盘上存储文件中的地址,称为 原始存储地址 或 物理地址,表示距离文件头的偏移。 
针对加载到内存以后映象中的地址,称为 相对虚拟地址(RVA),表示相对内存映象头的偏移。 
 
RVA 是当PE 文件被装到内存中后,某个数据位置相对于文件头的偏移量。
VA 
 
Virtual Address。虚拟地址,程序在虚拟内存中被装载的位置
ImageBase:基址 
 
PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼”优先”表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。简而言之,就是指定PE文件载入内存时,优先尝试载入的内存起始地址。
作为web手,来看这些的时候,不得不重新审视一下结构体
首先结构体其实类似于数组,也是一段连续的内存块,只不过他内存块中每一块的大小由结构体成员决定
c语言中结构体访问成员的操作,实际转化为汇编中访问 :结构体的基址+成员的偏移量(一般都是一个立即数)
如果已知结构体的定义,并且有结构体在内存中的基址,就可以解析对应的内存区域并读取其中的数据结构。(这些偏移量是在编译期由编译器根据结构体定义、数据类型大小以及对齐规则计算得到的)
PE结构可以大致分为:
DOS部分 
PE文件头 
节表(块表) 
节数据(块数据) 
调试信息 
 
结构如图:
 
PE 指纹
首先是根据文件的前两个字节是否为4D 5A,也就是’MZ’, 
然后后面还存在50 45,也就是PE 
 
 
DOS头 	DOS部分主要是为了兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)组成,PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,其结构如下:
(WORD 2字节 16位
DWORD 4字节 32位)
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; 
 
如果为DOS系统就会执行,打印输出一句话
然后其中比较重要的是e_lfanew 字段 ,是PE文件头的偏移地址
解析的时候读取为char*  , 然后赋值给一个结构体变量即可
(相当于把指针赋值给结构体变量,能通过对应的属性偏移值来访问属性)
PE 头 PE 头也叫NT头
PE头位于DOS Stub 后面,是以PE00为起始标记`
对应c语言中如下结构体:
typedef  struct  _IMAGE_NT_HEADERS  {    DWORD Signature;		     IMAGE_FILE_HEADER FileHeader;	     IMAGE_OPTIONAL_HEADER32 OptionalHeader;		 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 
 
其中文件头:
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头,但里面包含了很多重要的信息
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; 
 
可选PE头中DataDirectory 数据目录又存放了很多表的地址,如导入\导出表、重定位表等等是一个十分重要的字段,之后会经常使用, 且DataDirectory是一个长度为16的数组
程序在硬盘(文件)和内存中的对齐方式不相同,导致会有rawSize
RAV与FOA 
pe文件在硬盘和内存中的对齐方式不尽相同,导致内存中的偏移(RAV)与在文件中的偏移(FOA )不相同(有些地方又把FOA称之为RAW)
很多时候字段表示的是在内存中的偏移,而我们解析pe文件的时候,只是将文件的内容放入一个char* 的buffer 中而不是加载进内存来运行,因此要将RAV转化为FOA
这些不同只存在于不同的区段(区段对齐大小不一样)
区段(节区)头 学过汇编就知道,一个可执行程序是分段的,指令存放在代码段,数据存放在数据段,等等还有其他的区段
常见节区有code、text、data、resource等。
(区段头就是例如数据段、代码段等等的区段的信息)
把PE文件创建成多个节区结构的好处是,可以保证程序的安全性。若把code与data放在一个节区中相互纠缠很容易引发安全问题,即使忽略过程中的烦琐。假设向字符串data写入数据时,由于某个原因导致溢出,那么其下的code就会被覆盖,应用程序就会崩溃。
类别 
访问权限 
 
 
code节区 
执行、读取权限 
 
data节区 
非执行、读写权限 
 
resource节区 
非执行、读取权限 
 
结构体如下:
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; 
 
解析第一个区段头使用官方定义的宏函数
IMAGE_FIRST_SECTION(pNtHeaders); 
 
在PE文件中,节区头部是一个连续的结构体数组,每个 IMAGE_SECTION_HEADER 结构体大小固定,因此可以通过简单递增指针的方式访问下一个节区。
简单解析的代码
void  ParsePEFromBuffer (char  *buffer, bool  debug)  {         PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;          if  (pDosHeader->e_magic != 0x5A4D ) {         printf ("不是有效的PE文件\n" );         delete[] buffer;         return ;     }          PIMAGE_NT_HEADERS pNtHeaders =(PIMAGE_NT_HEADERS) (pDosHeader->e_lfanew +(uintptr_t ) buffer);      if  (0x4550  != pNtHeaders->Signature) {         printf ("不是有效的PE文件结构" );         delete[] buffer;         return ;     }          PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;     if  (debug) {         printf ("Machine:%x\n" , pFileHeader->Machine);         printf ("区段数:%d\n" , pFileHeader->NumberOfSections);         printf ("可选头大小:%d\n" , pFileHeader->SizeOfOptionalHeader);     }          PIMAGE_OPTIONAL_HEADER pOptionHeader=&pNtHeaders->OptionalHeader;     if (debug){         if (0x20b ==pOptionHeader->Magic){             printf ("64位程序\n" );         } else {             printf ("32位程序\n" );         }         printf ("程序入口地址偏移:%x\n" ,pOptionHeader->AddressOfEntryPoint);     }          PIMAGE_SECTION_HEADER  pSectionHeader= IMAGE_FIRST_SECTION(pNtHeaders) ;     for (int  i=0 ;i<pFileHeader->NumberOfSections;i++){                  char  name[9 ]{0 };                  memcpy_s(name,9 ,pSectionHeader->Name,8 );         printf ("----------------第%d个区段--------------------------\n" ,i+1 );         printf ("名称:%s\n" ,name);         printf ("内存地址偏移: %x\n" ,pSectionHeader->VirtualAddress);         printf ("区段大小:%d\n" ,pSectionHeader->SizeOfRawData);                  pSectionHeader++;     } 
 
FOA 和RAV转化  
经过上面变化,才有了RAV和FOA区分,但是只是各个区段部分,发生了变化,PE头和DOS头其实还是没变,DOS头和PE头中RAV=FAO的
	大多数的时候都是RAV,如何将其转化为FOA?
尽管相对于的文件头的偏移变了,但是相对于区段起始的偏移并未改变
因此可以得到公式:
FOA-所在区段FOA = RAV- 所在区段的RAV 	=> FOA = RAV- 所在区段的RAV + 所在区段FOA 
 
还有一点就是如何确定所在的区段,简单的方法就是遍历区段表
比较是否在区段的RAV范围:
RAV>=pSectionHeader->VirtualAddress && RAV< pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize 
 
转化代码:
DWORD CPeUtil::RavToFoa (DWORD RAV)  {          PIMAGE_SECTION_HEADER  pSectionHeader=IMAGE_FIRST_SECTION(peHeader->ntHeaders);         for (int  i=0 ;i<peHeader->ntHeaders->FileHeader.NumberOfSections;i++){         if  (RAV>=pSectionHeader->VirtualAddress && RAV< pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize){             return  RAV-pSectionHeader->VirtualAddress+pSectionHeader->PointerToRawData;         }         pSectionHeader++;     }     return  0 ; } 
 
导入/导出表 导出表 数据目录DataDirectory是可选PE头中的字段是一个数组,每一个元素都指向了一些结构(如导入、导出表、重定位表)
数据目录元素的结构体IMAGE_DATA_DIRECTORY如下:
typedef  struct  _IMAGE_DATA_DIRECTORY  {      DWORD VirtualAddress;			       DWORD Size;	     } IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY; 
 
dll动态链接库中需要的导出的函数,都会写入导出表 ,导出表是数据目录表中的第一个元素
然而不是只有dll有导出表(一般情况下exe没有导出表)
导出表IMAGE_EXPORT_DIRECTORY 结构体如下:
typedef  struct  _IMAGE_EXPORT_DIRECTORY  {      DWORD Characteristics;       DWORD TimeDateStamp;       WORD MajorVersion;       WORD MinorVersion;       DWORD Name;						       DWORD Base;						       DWORD NumberOfFunctions;			       DWORD NumberOfNames;				       DWORD AddressOfFunctions;			       DWORD AddressOfNames;				       DWORD AddressOfNameOrdinals;		     } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY; 
 
按照名称取出函数(地址表、序列表、名称表之间的关系)
名称表中函数名称所在的索引-> 序列表中对应元素中的序列号 -> 地址表中所对应的元素
解析导出表代码:
list <PExportFunc> CPeUtil::GetExportTable ()  {    list <PExportFunc> exportFuncList;      IMAGE_DATA_DIRECTORY exportDirectory =pOptionHeader->DataDirectory[0 ];     if  (exportDirectory.VirtualAddress==0 ){                  printf ("not exist export table\n" );         return  exportFuncList;     }      DWORD offset= RavToFoa(exportDirectory.VirtualAddress);     if  (offset==0 ){                  printf ("not found export table\n" );         return  exportFuncList;     }     PIMAGE_EXPORT_DIRECTORY peExport=PIMAGE_EXPORT_DIRECTORY (buffer+offset);     char * dllName=RavToFoa(peExport->Name)+ buffer;     printf ("dll name:%s\n" ,dllName);          DWORD* funcAddr=(DWORD *)(RavToFoa(peExport->AddressOfFunctions)+buffer);          WORD* ordinal= (WORD*)(RavToFoa(peExport->AddressOfNameOrdinals)+buffer);          DWORD* names=(DWORD *)(RavToFoa(peExport->AddressOfNames)+buffer);     for  (int  i=0 ;i<peExport->NumberOfFunctions;i++){         PExportFunc func=new  ExportFunc{};         func->VirtualAddress=funcAddr;         for (int  j=0 ;j<peExport->NumberOfNames;j++){             if (ordinal[j]==i){                                 char * name=  RavToFoa(names[j])+buffer;                func->Ordinal=ordinal[j];                func->Name=name;                break ;             }         }         exportFuncList.push_back(func);         funcAddr++;     }     return  exportFuncList; } 
 
导入表 简单的就是:告诉系统你需要用到哪些dll,用哪些函数。
需要导入调用外部函数
调用多个dll就会有如下多个_IMAGE_IMPORT_DESCRIPTOR结构,导入描述符
typedef  struct  _IMAGE_IMPORT_DESCRIPTOR  {      union  { 	DWORD Characteristics;		 	DWORD OriginalFirstThunk;	       } DUMMYUNIONNAME;       DWORD TimeDateStamp;       DWORD ForwarderChain;	       DWORD Name;			       DWORD FirstThunk;		     } IMAGE_IMPORT_DESCRIPTOR; 
 
上述结构体中的OriginalFirstThunk和FirstThunk分别指向其对应的INT表和IAT表
IAT (Import Address Table) 导入函数地址表 
INT(Import Name Table)导入函数名称表 
 
加载DLL的方式实际有两种:一种是显示链接(Explicit Linking),程序使用DLL时加载,函数使用完毕时再释放内存; 一种是隐式链接(Implicit Linking),程序开始时就加载DLL,程序终止时再释放占用的内存。(以后遇到再说(:
 
OriginalFirstThunk: 指向IMAGE_THUNK_DATA结构数组的RVA, 其内容在程序未运行下, 和OriginalFirstThunk内容一样,,如下:
typedef  struct  _IMAGE_THUNK_DATA64  {      union  { 	ULONGLONG ForwarderString; 	ULONGLONG Function;		 	ULONGLONG Ordinal;		 	ULONGLONG AddressOfData;        } u1;     } IMAGE_THUNK_DATA64;     typedef  IMAGE_THUNK_DATA64 *PIMAGE_THUNK_DATA64; 
 
这4个成员是一个共用体, 在不同情况下代表不同的数据
这个值最高位为1的时候,表示函数是一个序号输出值, 低31位会被看做API的导出序号, 当最高位为0时, 这时候这个值是一个指向IMAGE_IMPORT_BY_NAME结构的RVA
typedef  struct  _IMAGE_IMPORT_BY_NAME  {      WORD Hint;       CHAR Name[1 ];      } IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME; 
 
FirstThunk : 指向IAT,不过在不同的情况下IAT内容不一样。
当TimeDateStamp为0的时候表示未绑定,该字段其实跟OriginalFirstThunk 指向的差不多,对应上图PE加载前
当TimeDateStamp不为0,这时候指向的是函数真实地址表,对应上图PE加载后
解析代码:
typedef  struct  ImportFunc {    char * dllName;      bool  useName;       char * name;          DWORD Ordinal;   }ImportFunc,*PImportFunc; 
 
解析函数,(接着前面的代码)
list<PImportFunc> CPeUtil::GetImportTable ()   {    list<PImportFunc> importFuncList;     IMAGE_DATA_DIRECTORY importDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];          PIMAGE_IMPORT_DESCRIPTOR pImportTable=(PIMAGE_IMPORT_DESCRIPTOR)(RavToFoa (importDirectory.VirtualAddress)+buffer);     while  (pImportTable->OriginalFirstThunk){                  char  * dllName= RavToFoa (pImportTable->Name)+buffer;         PIMAGE_THUNK_DATA pThunkData=(PIMAGE_THUNK_DATA)(RavToFoa (pImportTable->OriginalFirstThunk)+buffer);                  while  (pThunkData->u1.Function) {             PImportFunc  func=new  ImportFunc  ();                          if  (pThunkData->u1.Ordinal & 0x80000000 ) {                                                   func->dllName=dllName;                 func->useName= false ;                 func->Ordinal=pThunkData->u1.Ordinal & 0x7FFFFFFF ;             } else  {                 PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME) (RavToFoa (pThunkData->u1.AddressOfData) +                 buffer);                                  func->dllName=dllName;                 func->useName= true ;                 func->name = pImportByName->Name;             }             importFuncList.push_back (func);             pThunkData++;         }         pImportTable++;     }     return  importFuncList; } 
 
重定位表 重定位(Relocation ):代码重定位是把可执行代码从内存的一块区域移动到另外一块地方。但是如果指令中某些操作数没有随着地址的改变而改变,这样势必导致运行出错。如:全局变量的地址包含在机器码中,而局部变量没有包含绝对地址。
	重定位信息是在编译时期由编译器生成,并且保存在应用程序中,在程序执行的时候由操作系统予以修正。如果在装载时该位置已经被别的应用程序使用,操作系统会重新选择一个新的基地址。此时,就需要对所有重定位信息进行纠正,纠正的依据就是PE中的重定位表。
重定位表是数据目录中的第6项,索引为5
对应IMAGE_BASE_RELOCATION结构体:
typedef  struct  _IMAGE_BASE_RELOCATION  {DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION; typedef  IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;
 
其结构也如下:
 
解析代码:
list<DWORD> CPeUtil::GetReLocation ()   {    list<DWORD> rvaList;     IMAGE_DATA_DIRECTORY relocationDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];     PIMAGE_BASE_RELOCATION pRelocation=(PIMAGE_BASE_RELOCATION)(RavToFoa (relocationDirectory.VirtualAddress)+buffer);     while  (1 ){         if (pRelocation->VirtualAddress==0 ){             break ;         }                  DWORD* prelocOffset =(DWORD*)pRelocation+4 ;                  DWORD number= (pRelocation->SizeOfBlock-sizeof (IMAGE_BASE_RELOCATION))/2 ;         for (int  i=0 ;i<number;i++){                          if ((*prelocOffset & 0x3000 )== 0x3000 ){                                  DWORD rva =((*prelocOffset) & 0x0FF ) + pRelocation->VirtualAddress;                 rvaList.push_back (rva);             }             prelocOffset++;         }         pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t )pRelocation+pRelocation->SizeOfBlock);     }     return  rvaList; } 
 
然后中间的部分高四位表示的是类型。低十二位表示的重定位地址
解析代码
list<DWORD> CPeUtil::GetReLocation ()   {    list<DWORD> rvaList;     IMAGE_DATA_DIRECTORY relocationDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];     PIMAGE_BASE_RELOCATION pRelocation=(PIMAGE_BASE_RELOCATION)(RavToFoa (relocationDirectory.VirtualAddress)+buffer);     while  (1 ){         if (pRelocation->VirtualAddress==0 ){             break ;         }                  DWORD* prelocOffset =(DWORD*)((uintptr_t )pRelocation + sizeof (IMAGE_BASE_RELOCATION));;                  DWORD number= (pRelocation->SizeOfBlock-sizeof (IMAGE_BASE_RELOCATION))/2 ;         for (int  i=0 ;i<number;i++){                          if ((*prelocOffset & 0x3000 )== 0x3000 ){                                  DWORD rva =((*prelocOffset) & 0x0FF ) + pRelocation->VirtualAddress;                 rvaList.push_back (rva);             }             prelocOffset++;         }         pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t )pRelocation+pRelocation->SizeOfBlock);     }     return  rvaList; } 
 
扩展学习,可以看看另一位师傅 的文章:
PE文件结构从初识到简单shellcode注入 
Reference