跟着王哥学逆向——PE文件详解篇(第一篇)

这是《跟着王哥学逆向》PE文件详解篇第一篇,该篇章主要对PE文件内部结构进行仔细剖析,同时记录下来PE文件各个字节所代表的含义,方便自己查看。此篇章对DOS头、PE文件头、区块表等按照顺序介绍,有很强的系统性,计划下一篇章对导入表、导出表、资源表、重定位表进行详解。该系列学习主要参考小甲鱼讲的PE系列视频,真的很顶。

PE文件是什么

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。那Windows是怎么区分可执行文件和非可执行文件的呢?我们调用LoadLibrary传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。下图即PE文件结构图:
在这里插入图片描述

DOS头

组成
DOS头由MZ文件头和Dos Stub两部分组成。无论是32位或64位可执行文件,其文件的头部必定是IMAGE_DOS_HEADER。

MZ头
IMAGE_DOS_HEADER 结构体,其大小占64个字节,并且该结构中的最后一个LONG类型e_lfanew成员指向PE文件头的位置为中的PE文件头标志的地址。
这里有两个比较有用的成员信息:
1、e_magic,用于判断PE文件的标识。如果不是MZ即不是十六进制值:0x5A4D。计算机存储顺序是低位在前高位在后,所以存储为:0x4D5A。
在这里插入图片描述
2、e_lfanew,这里是指pe的偏移量,用于找到pe头的位置。
在这里插入图片描述
IMAGE_DOS_HEADER 数据结构定义如下:

//注释掉的不需要重点分析
struct _IMAGE_DOS_HEADER{
     0X00 WORD e_magic;      //※Magic DOS signature MZ(4Dh 5Ah):MZ标记:用于标记是否是可执行文件
     //0X02 WORD e_cblp;     //Bytes on last page of file
     //0X04 WORD e_cp;       //Pages in file
     //0X06 WORD e_crlc;     //Relocations
     //0X08 WORD e_cparhdr;  //Size of header in paragraphs
     //0X0A WORD e_minalloc; //Minimun extra paragraphs needs
     //0X0C WORD e_maxalloc; //Maximun extra paragraphs needs
     //0X0E WORD e_ss;       //intial(relative)SS value
     //0X10 WORD e_sp;       //intial SP value
     //0X12 WORD e_csum;     //Checksum
     //0X14 WORD e_ip;       //intial IP value
     //0X16 WORD e_cs;       //intial(relative)CS value
     //0X18 WORD e_lfarlc;   //File Address of relocation table
     //0X1A WORD e_ovno;     //Overlay number
     //0x1C WORD e_res[4];   //Reserved words
     //0x24 WORD e_oemid;    //OEM identifier(for e_oeminfo)
     //0x26 WORD e_oeminfo;  //OEM information;e_oemid specific
     //0x28 WORD e_res2[10]; //Reserved words
     0x3C DWORD e_lfanew;    //※Offset to start of PE header:定位PE文件,PE头相对于文件的偏移量
};
//一个word是对应2个字节,该结构体大小为64个字节,因为里面定义了两个word型数组。

DOS stub
dos存根,在IMAGE_DOS_HEADER和IMAGE_NT_HEADERS之间存在一DOS存根,这其实是一段汇编代码:PE文件是运行在32位或64位操作系统下的。其功能是当该EXE运行在16位环境下,输出一段文字:“This program cannot be run in DOS mode”,然后并退出该进程。
在pe文件利用的时候,我们可以把payload写入到当前区域,诸如存放我们的shellcode,在读取时,获取dos头字节数,减去MZ头字节数,即为dos存根字节大小。然后拿去操作加载shellcode等。
在这里插入图片描述

PE头

紧跟着DOS stub的时PE头文件(PE Header)。PE Header是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。
PNTHeader = ImageBase + dosHeader->e_lfanew

PE头的数据结构被定义为IMAGE_NT_HEADERS。包含三部分,其结构如下:

typedef struct IMAGE_NT_HEADERS{  
     DWORD Signature;  
     IMAGE_FILE_HEADER FileHeader;  
     IMAGE_OPTIONAL_HEADER32 OptionalHeader;  
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS;   

Signature
将文件标识为 PE 映像的 4 字节签名。字节为“PE\0\0”。这个字段是PE文件的标志字段,通常设置成00004550h,其ASCII码为PE00,这个字段是PE文件头的开始,前面的DOS_HEADER结构中的字段e_lfanew字段就是指向这里。
在这里插入图片描述
IMAGE_FILE_HEADER
IMAGE_FILE_HEADER(映像头文件)结构包含了文件的物理层信息及文件属性。共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;  

Machine字段
常见【机器:标识】:Intel I386 14ch、MIPS R3000 162h、Alpha AXP 184h、Power PC 1F0h、MIPS R4000 184h
比如这里的014c,就是在Intel I386机器上运行。
在这里插入图片描述
NumberOfSection:标识区块的数目,关于区块后面会详细讲。
在这里插入图片描述
TimeDateStamp:这个字段没啥好说的,指的就是PE文件创建的事件,这个时间是指从1970年1月1日到创建该文件的所有的秒数。
PointerToSymbolTable:这个字段用的比较少,略
NumberOfSymbol:这个字段也用得很少,略
SizeOfOptionalHeader:紧跟着IMAGE_FILE_HEADER后面的数据大小,这也是一个数据结构,它叫做IMAGE_OPTIONAL_HEADER,其大小依赖于是64位还是32位文件。32位文件值通常是00E0h,对于64位值通常为00F0h。
在这里插入图片描述
Characteristics:文件属性,普通EXE文件这个字段值为010fh,DLL文件这个字段一般是0210h。保存在winnt.h头文件中有个有个表,然后其中有一些二进制及其代表的属性,可以用或的方式去组合这些属性。
在这里插入图片描述
代码如下:

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

IMAGE_OPTIONAL_HEADER结构
IMAGE_OPTIONAL_HEADER(可选映像头)是一个可选的机构,实际上IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据。 总共224个字节,最后128个字节为数据目录(Data Directory) ,其结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER 
{
  //
  // Standard fields. 
  //
+18h  WORD  Magic;     // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah  BYTE   MajorLinkerVersion;   // 链接程序的主版本号
+1Bh  BYTE   MinorLinkerVersion;   // 链接程序的次版本号
+1Ch  DWORD  SizeOfCode;   // 所有含代码的节的总大小
+20h  DWORD  SizeOfInitializedData;  // 所有含已初始化数据的节的总大小
+24h  DWORD  SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h  DWORD  AddressOfEntryPoint;  // 程序执行入口RVA
+2Ch  DWORD  BaseOfCode;   // 代码的区块的起始RVA
+30h  DWORD  BaseOfData;   // 数据的区块的起始RVA
  //
  // NT additional fields.  以下是属于NT结构增加的领域。
  //
+34h  DWORD  ImageBase;   // *********程序的首选装载地址
+38h  DWORD  SectionAlignment;   // *********内存中的区块的对齐大小
+3Ch  DWORD  FileAlignment;   // *********文件中的区块的对齐大小
+40h  WORD  MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h  WORD  MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h  WORD  MajorImageVersion;    // 可运行于操作系统的主版本号
+46h  WORD  MinorImageVersion;    // 可运行于操作系统的次版本号
+48h  WORD  MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah  WORD  MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch  DWORD  Win32VersionValue;    // 莫须有字段,不被病毒利用的话一般为0
+50h  DWORD  SizeOfImage;    // 映像装入内存后的总尺寸
+54h  DWORD  SizeOfHeaders;    // 所有头 + 区块表的尺寸大小
+58h  DWORD  CheckSum;    // 映像的校检和
+5Ch  WORD  Subsystem;    // 可执行文件期望的子系统
+5Eh  WORD  DllCharacteristics;    // DllMain()函数何时被调用,默认为 0
+60h  DWORD  SizeOfStackReserve;    // 初始化时的栈大小
+64h  DWORD  SizeOfStackCommit;    // 初始化时实际提交的栈大小
+68h  DWORD  SizeOfHeapReserve;    // 初始化时保留的堆大小
+6Ch  DWORD  SizeOfHeapCommit;    // 初始化时实际提交的堆大小
+70h  DWORD  LoaderFlags;    // 与调试有关,默认为 0 
+74h  DWORD  NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来    // 一直是16
+78h  DWORD  DataDirctory[16];     // ********* 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Magic:表示可选头的类型。
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
在这里插入图片描述
AddressOfEntryPoint
程序入口的RVA,对于exe可以理解为WinMain的RVA。对于DLL可以理解为DllMain的RVA,对于驱动程序,可以理解为DriverEntry的RVA。写壳的时候这个地址相当重要。
可选字段部分ImageBase
映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快。当文件被装载到其他地址时,进行重定位操作,会慢一点。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入。这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。
因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1,即DLL中不删除重定位信息,EXE文件中删除重定位信息。
注意:#defineIMAGE_FILE_RELOCS_STRIPPED 0x0001
//Relocation info stripped from file.(从文件中删除重定位信息。)
在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。
在这里插入图片描述
SectionAlignment
节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
FileAlignment
节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
实际上这两个对齐方式很重要,在后续RVA和偏移地址的转化中有用到。
NumberOfRvaAndSizes
数据目录的项数,即下面这个数组的项数。
在这里插入图片描述
DataDirectory
数据目录,这是一个数组,数组的项定义如下:

typedefstruct _IMAGE_DATA_DIRECTORY {
   DWORD   VirtualAddress;
   DWORD   Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;VirtualAddress

是一个RVA,Size。这两个数有什么用呢?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   //0 导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   //1 导入表   
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   //2 资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   //3 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   //4 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   /5 重定位基本表
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory  
         // IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)  
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data  
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP  
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory  
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory  
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers  
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table  
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors  
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor  

对应输入表位置和大小在这里插入图片描述
采用PEinfo所查到输入表位置与大小
在这里插入图片描述
而对于我们安全人员来讲,导入表和导出表相当重要。

区块表(节表)

节表是PE文件后续节的描述,Windows根据节表的描述加载每个节。PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中IMAGE_SECTION_HEADER结构数量等于节的数量加一。
节表总是被存放在紧接在PE文件头的地方。节表中 IMAGE_SECTION_HEADER结构的总数总是由PE文件头IMAGE_NT_HEADERS(注:即本资料中的NT头) 结构中的FileHeader.NumberOfSections 字段来指定的。
块表结构体如下:【一个区块表结构是40个字节】

typedef struct _IMAGE_SECTION_HEADER {  
   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;  

(1)Name:区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个$ 的区块名字会从连接器那里得到特殊的待遇,前边带有$ 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
在这里插入图片描述
(2) VirtualSize:对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
在这里插入图片描述
(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
在这里插入图片描述
(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
在这里插入图片描述
(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
在这里插入图片描述
上述四个数据很重要,在分析的时候经常用到,因此需要熟悉其代表的具体意义。
PointerToRawDate+SizeofRawData=下一块偏移地址
(6) PointerToRelocations 在PE中无意义
(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
(8) NumberOfRelocations 在PE中无意义
(9) NumberOfLinenumbers 该块在行号表中的行号数目
(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
在这里插入图片描述

区块表拓展

区块名称以及意义:
我们在Visual C++中也可以自己命名我们的区块,用#pragma 来声明,告诉编译器插入数据到一个区块内,格式如下:
#pragma data_msg( “FC_data” )
以上语句告诉编译器将数据都放进一个叫“FC_data”的区块内,而不是默认的.data 区块。区块一般是从OBJ 文件开始,被编译器放置的。链接器的工作就是合并左右OBJ 和库中需要的块,使其成为一个最终合适的区块。链接器会遵循一套相当完整的规则,它会判断哪些区块将被合并以及如何被合并。
链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。事实上合并区块有一个好处就是可以节省磁盘的内存空间……注意:我们不应该将.rsrc、.reloc、.pdata 合并到··的区块里。
下面是常用区块及其意义:
在这里插入图片描述在这里插入图片描述
块的偏移地址
块起始地址在磁盘中是按照IMAGE_OPTIONAL_HEADER32中的FileAlignment字段的值进行对齐的,而当被加载到内存中时是按照同一结构中的SectionAlignment字段的值设置对齐的,两者的值可能不同。所以一个块表被装载到内存后相对于文件头的偏移地址和磁盘中的偏移地址可能是不同的。
区块的对齐值
之前我们简单了解过区块是要对齐的,无论是在内存中存放还是在磁盘中存放,但他们一般的对齐值是不同的。
PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。
例如,在PE文件中,一个典型的对齐值是200h,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。
PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。
一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。
RVA 和文件偏移的转换
RVA 是相对虚拟地址(RelativeVirtual Address)的缩写,顾名思义,它是一个“相对地址”。PE 文件中的各种数据结构中涉及地址的字段大部分都是以 RVA 表示的。
更为准确的说,RVA 是当PE 文件被装载到内存中后,某个数据位置相对于文件头的偏移量。举个例子,如果 Windows 装载器将一个PE 文件装入到 00400000h 处的内存中,而某个区块中的某个数据被装入 0040··xh 处,那么这个数据的 RVA 就是(0040··xh - 00400000h )= ··xh,反过来说,将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。
换算 RVA 和文件偏移
从内存中地址定位到硬盘中文件的地址,可以用来定位更改源程序
当处理PE 文件时候,任何的 RVA 必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,唯一可用的方法就是最土最笨的方法:
步骤一:循环扫描区块表得出每个区块在内存中的起始 RVA(根据IMAGE_SECTION_HEADER 中的VirtualAddress 字段),并根据区块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData 字段)算出区块的结束 RVA(两者相加即可),最后判断目标 RVA 是否落在该区块内。
步骤二:通过步骤一定位了目标 RVA 处于具体的某个区块中后,那么用目标 RVA 减去该区块的起始 RVA ,这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.
步骤三:在区块表中获取该区块在文件中所处的偏移地址(根据IMAGE_SECTION_HEADER 中的PointerToRawData 字段), 将这个偏移值加上步骤二得到的 RVA2 值,就得到了真正的文件偏移地址。

PE文件加载过程

在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,Windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
在这里插入图片描述
当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”【补的0】。
在这里插入图片描述
Windows 装载器在装载DOS部分,PE文件头部分和节表(节表也称为区块表,块表)部分是不进行任何特殊处理的,而在装载节(节也称为区块)的时候则会自动按节(区块)的属性的不同做不同的处理。
在这里插入图片描述
然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到对应的指令中。例如在IDA里看上去是这个样子:在这里插入图片描述
这种表示方式叫做虚拟地址(VA)。
也许有人要问,既然有VA这么简单的表示方式为什么还要有前面的RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。【不太理解】

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值