PE总结

个人觉得PE结构中RVA–>RAW、IAT、EAT等是较难理解的内容,在此做一个笔记(本文中部分内容来自网络,侵删)

PE

PE(Portable Executable)文件是运行与windows系统下的可执行文件格式,像我们平常所见到的.exe、.dll都是PE文件,除此之外,像.sys(驱动文件)、.obj(对象文件)等都是PE文件。
这里写图片描述

个人觉得,在学习PE结构最难的就是理解硬盘文件–>内存文件,PE结构中定义了许多结构体,这些结构体中定义的一些量,有些是已经确定的,有些是当文件被PE加载器,加载后再确定的(内存中的);

一、RVA、VA

RVA(相对虚拟地址)这是一个概念,代表一类地址,RVA是为了更好的表示(硬盘->内存)地址。每个RVA都有它相对参考的值(不同位置的RVA所参考的值(基准位置)不一定相同)

VA指的是进程虚拟内存的绝对地址,即为实际的内存地址(VA),RVA(relative virtual address)是指从某个基准位置(ImageBase)开始的相对地址,PE头内部信息大多以RVA形式存在。原因是在一个可执行文件中,有许多在内存中的地址必须被指定的位置。PE文件可以被加载到进程地址空间的任何位置。当PE文件(主要是DLL,因为每个exe文件总是使用独立的虚拟地址空间)加载到进程虚拟内存的特定位置时,该位置可能已经被占用。此时必须重定位(Relocation)到其他空白位置,若PE头信息使用VA,则无法正常访问,而使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问。
VA = 基地址 + RVA

RVA是需要加上基地址(imagebase)才能获得线性地址的数值。基地址就是PE映象文件被装入内存的地址,并且可能会随着一次又一次的调用而变化。
PE文件可以被加载到进程地址空间的任何位置。当PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经被占用。此时必须重定位(Relocation)到其他空白位置,若PE头信息使用VA,则无法正常访问,而使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问。

文件偏移地址(File Offset Address,FOA)和内存无关,他是指某个位置距离文件头的偏移。

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;           // PE00
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

二、几个结构

2.1、IMAGE_FILE_HEADER

typedef struct _IMAGE_FILE_HEADER {
WORD    Machine;
WORD    NumberOfSections;
DWORD   TimeDateStamp;
DWORD   PointerToSymbolTable;
DWORD   NumberOfSymbols;
WORD    SizeOfOptionalHeader;       //指出IMAGE_OPRIONAL_HEADER32结构体的长度
WORD    Characteristics;               //表示文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

2.2、IMAGE_OPRIONAL_HEADER32

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD   VirtualAddress;
DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;


typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD    Magic;
BYTE    MajorLinkerVersion;
BYTE    MinorLinkerVersion;
DWORD   SizeOfCode;
DWORD   SizeOfInitializedData;
DWORD   SizeOfUninitializedData;
DWORD   AddressOfEntryPoint;       //EP 
DWORD   BaseOfCode;
DWORD   BaseOfData;

//
// NT additional fields.
//

DWORD   ImageBase;                 //基地址
DWORD   SectionAlignment;          //内存中节区的对齐值(存在的最小单位)
DWORD   FileAlignment;             //磁盘文件的对齐值   内存节区/磁盘文件一定是~Alignment的整数倍,不足用0填充
WORD    MajorOperatingSystemVersion;
WORD    MinorOperatingSystemVersion;
WORD    MajorImageVersion;
WORD    MinorImageVersion;
WORD    MajorSubsystemVersion;
WORD    MinorSubsystemVersion;
DWORD   Win32VersionValue;
DWORD   SizeOfImage;            //当PE加载到内存中时,其指定PE image在内存中所占的大小
DWORD   SizeOfHeaders;             //整个PE头的大小
DWORD   CheckSum;
WORD    Subsystem;                //来区分是驱动文件(.sys)还是普通的可执行文件(.exe,.dll)
WORD    DllCharacteristics;
DWORD   SizeOfStackReserve;
DWORD   SizeOfStackCommit;
DWORD   SizeOfHeapReserve;
DWORD   SizeOfHeapCommit;
DWORD   LoaderFlags;
DWORD   NumberOfRvaAndSizes;  //指定DateDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//是一个由IMAGE_DATA_DIRECTORY结构体组成的数组,其中第一、第二个数组分别指向EAT、IAT的起始地址(相当重要)
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

2.3、IMAGE_SECTION_HEASER(40byte)

#define IMAGE_SIZEOF_SHORT_NAME  8

typedef struct _IMAGE_SECTION_HEADER {
BYTEName[IMAGE_SIZEOF_SHORT_NAME];     //节区名称
union {
DWORD   PhysicalAddress;
DWORD   VirtualSize;                //内存中节区的大小
} Misc;
DWORD   VirtualAddress;             //(RVA)内存中节区的起始位置
DWORD   SizeOfRawData;              //硬盘文件中节区的大小
DWORD   PointerToRawData;          //磁盘文件中节区的起始位置
DWORD   PointerToRelocations;
DWORD   PointerToLinenumbers;
WORD    NumberOfRelocations;
WORD    NumberOfLinenumbers;
DWORD   Characteristics;           //节区属性(由or连接)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

三、几个概念

RVA–> RAW(文件偏移地址)

该部分参考了看雪论坛的一篇文章,帮助理解

只有以节的起始地址为参考的相对偏移才是固定的

通常我们使用工具对PE文件进行分析,得到一个字符串的地址(内存中的mRVA),要想在文件中找到它,需要找到这个mRVA所在的节区,用mRVA减去这节区的起始位置(即VirtualAddress),得到的这个值再加上,文件中此节区的起始地址,就得到了文件偏移

iRVA:该RVA是以ImageBase为参考的,iRVA = VA - ImageBase (我们常说为RVA)

mRVA:该RVA是以VOffset(如.text节的起始地址)为参考的;

真正的文件偏移为fRVA;

虚拟地址转文件偏地址fRVA fRVA = mRVA + ROffset

由于mRVA(内存节偏移)= iRVA - VOffset,再由于iRVA = 虚拟地址-ImageBase,

所以,mRVA = (虚拟地址 - ImageBase) - VOffset;

所以,fRVA = (虚拟地址 - ImageBase) - VOffset + ROffset;

最终得到:
fRVA = (虚拟地址 - ImageBase) - VOffset + ROffset

内存偏移 - 该段起始的RVA(VirtualAddress) = 文件偏移 - 该段的PointerToRawData

四、IAT和EAT

4.1、IAT(Import_Address_Table)导出地址表

每一个结构体就代表存在一个库函数(xxx.dll),这些库函数组成了IID(IMAGE_IMPORT_DESCRIPTOR的简称) 数组结构。在这个IID 数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL的IID 作为结束的标志。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {    //20Byte
union {
DWORD   Characteristics;          // 0 for terminating null import descriptor
DWORD   OriginalFirstThunk;      // (RVA)INT address (import name table)
};
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;      
DWORD   Name;                //库(dll)名称的地址(RVA)
DWORD   FirstThunk;        // (RVA) IAT address (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

//INT

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD     Hint;                       //Ordinal序号
BYTE     Name[1];                   //函数的名称 
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • OriginalFirstThunk –> INT–>IMAGE_IMPORT_BY_NAME

    • INT是一个包含导入函数信息(Ordinal,Name)的结构体指针数组,只有获得了这些信息,才能在加载到进程内存的库中准确求出相应函数的起始地址.INT由地址数组组成(数组以NULL结尾)
  • FirstThunk(IAT的地址) –> IAT(在内存和硬盘中值不同)

当IAT地址值的最高位为1时,表示函数以序号方式输入,这时候低31位被看作一个函数序号.当IMAGE_THUNK_DATA值的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME 结构

PE装载器把导入函数输入到IAT(真正IAT)的顺序
1.先从IID的Name中读取需要的dll 的名称
2.装载到相应库,用LoadLibrary("xxxx.dll") 
3.在从OriginalFirstThunk中得到INT的地址
4.逐一读取INT数组中的值,获取IMAGE_IMPORT_BY_NAME的地址
5.获取函数的起始位置(通过Hint或Name项)
6.读取IID中FirstThunk,得到IAT地址
7.将上面获得的函数地址赋给相应的IAT数组值
8.重复4-7,直到INT结束(遇到NULL)(去查函数)

通俗一点说,先找到需要的dll 文件,在通过OriginalFirstThunk找到INT(此值不能被改写),获得函数的起始地址,再从FirstThunk获得IAT的地址,最后将5中获取的函数地址赋给IAT数组。开始的时候地址未知,当Windows加载器加载到内存中才会填充。先找dll,再找到具体函数,再填充IAT

4.2、EAT 导出表

主要存在于dll文件中,为不同的应用程序提供库文件(dll)中的函数

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD   Characteristics;
DWORD   TimeDateStamp;
WORD    MajorVersion;
WORD    MinorVersion;
DWORD   Name;                         //库名称的地址(RVA)
DWORD   Base;                          //Ordial base 基数 
DWORD   NumberOfFunctions;            //Export函数的实际个数(RVA)  
DWORD   NumberOfNames;              //Export函数中具有函数名的函数个数(RVA)  
DWORD   AddressOfFunctions;       //Export函数地址数组(RVA)  
DWORD   AddressOfNames;                //函数名称地址数组(RVA)      
DWORD   AddressOfNameOrdinals;           //Ordinal地址数组(可以理解为导出函数名称的编号,可用于查找没有函数名称的导出函数)(RVA)  
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

EAT的转载过程

1.获取EAT的RVA
2.从EAT中获取base(起始序号)
3.将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引
4.与NumberOfFunctions数比较(不能超过它)
(5.用这个索引值在AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址)此为无名函数的查找方式
5.找到AddressOfNames --> 函数名称数组
6.通过比较string,查找到函数名,并记下这个函数名在字符串地址表中的索引值
7.在利用AddressOfNameOrdinals --> ordinal数组, 指向的数组中以同样的索引值取出数组项的值(该数+base与函数名一一对应)
8.利用该到AddressOfFunctions(此中函数顺序未知,要依靠索引)索引相应函数

未完待续

猜你喜欢

转载自blog.csdn.net/life_hes_az/article/details/79678874