PE文件格式

  1. PE文件是Windows操作系统下使用的可执行文件格式,它是微软在UNIX平台的COFF基础上制作而成的。
  2. PE文件是指32为的可执行文件,也成为PE32,64位的可执行文件称为PE+或PE32+。

PE文件格式

  1. PE文件种类
    在这里插入图片描述

  2. 下图是notepad.exe文件的起始部分,也是PE文件的头部分(PE header),notepad.exe文件运行需要的所有信息都存储在这个PE头中,如何加载到内存、从何处开始执行、运行中需要的DLL有哪些、需要多大的栈/堆内存,大量信息以结构体形式存储在PE头中。
    在这里插入图片描述

  3. 基本结构
    在这里插入图片描述

  • 从DOS头到节区头是PE头部分,其下的节区合称PE体,文件中使用偏移,内存中使用VA来表示位置。
  • 文件加载到内存时,情况就会发生变化(节区的大小、位置等),文件的内容一般可分为代码、数据、资源节,分别保存。
  • PE头与各节区的尾部都存在一个区域,称为NULL填充,为了提高处理文件、内存、网络包的效率,文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数上。
  1. VA&RVA
  • VA指的是进程虚拟内存的绝对地址,RVA指从某个基准位置开始的相对地址,VA与RVA满足下面的换算关系。
RVA + ImageBase = VA
  • PE头内部信息大多以RVA形式存在,原因在与,PE文件加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件,此时必须经过重定位,将其加载到其他空白的位置,若PE头信息使用的时VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息,不会出现任何问题。

PE头

  1. 创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑PE文件对DOS文件的兼容性,其结果是在PE头的最前面添加了一个IMAGE_DOS_HEADER结构体,用来拓展已有的DOS EXE头。
//(注:最左边是文件头的偏移量。)
IMAGE_DOS_HEADER STRUCT 
{ 
+0h WORD e_magic   // Magic DOS signature MZ(4Dh 5Ah)     DOS可执行文件标记 
+2h   WORD  e_cblp  // Bytes on last page of file    
+4h WORD  e_cp   // Pages in file 
+6h WORD  e_crlc   // Relocations 
+8h WORD  e_cparhdr   // Size of header in paragraphs 
+0ah WORD  e_minalloc  // Minimun extra paragraphs needs 
+0ch WORD  e_maxalloc  // Maximun extra paragraphs needs 
+0eh WORD  e_ss    // intial(relative)SS value      DOS代码的初始化堆栈SS 
+10h WORD  e_sp    // intial SP value                 DOS代码的初始化堆栈指针SP 
+12h WORD  e_csum    // Checksum 
+14h WORD  e_ip    //    intial IP value                     DOS代码的初始化指令入口[指针IP] 
+16h WORD  e_cs    // intial(relative)CS value                    DOS代码的初始堆栈入口 
+18h WORD  e_lfarlc    // File Address of relocation table 
+1ah WORD  e_ovno        //    Overlay number 
+1ch WORD  e_res[4]    // Reserved words 
+24h WORD  e_oemid    //    OEM identifier(for e_oeminfo) 
+26h WORD      e_oeminfo   //    OEM information;e_oemid specific  
+29h WORD  e_res2[10]   //    Reserved words 
+3ch DWORD   e_lfanew     // Offset to start of PE header             指向PE文件头 
} IMAGE_DOS_HEADER ENDS
  • IMAGE_DOS_HEADER结构体的大小为40各字节,在该结构体中必须知道2个重要成员:
    e_magic:DOS签名。
    e_lfanew:指示NT头的偏移。

  • 所有PE文件在开始部分都有DOS签名("MZ”),e_lfanew值指向NT头所在位置。
    在这里插入图片描述

  • 根据PE规范,文件开始的2个字节为4D5A,e_lfanew的值为000000e0.

  1. DOS存根
  • DOS存根位于DOS头下方,是个可选项,且大小不固定,即使没有DOS存根,文件也能正常运行,DOS存根由代码与数据混合而成。
    在这里插入图片描述

  • 文件偏移40~4D区域为16位汇编指令,32位的Windows OS中不会运行该命令,因此被识别位PE文件,所以完全忽略该代码,在DOS环境中运行Notepad.exe,可使其执行该代码。

  1. NT头
typedef struct _IMAGE_NT_HEADERS {
+00h    DWORD Signature; // 固定为 0x00004550  根据小端存储为:"PE.."
+04h    IMAGE_FILE_HEADER FileHeader;
+18h    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • IMAGE_NT_HEADERS结构体大小为F8,由3个成员组成,第一个成员为签名结构体,其值为50450000h("PE"00),另外两个成员分别为文件头与可选头结构体。
  1. NT头:文件头
typedef struct _IMAGE_FILE_HEADER {
+04h    WORD          Machine;              // 运行平台
+06h    WORD          NumberOfSections;     // 文件的区块数目
+08h    DWORD         TimeDateStamp;        // 文件创建日期和时间
+0Ch    DWORD         PointerToSymbolTable; // 指向符号表(主要用于调试)
+10h    DWORD         NumberOfSymbols;      // 符号表中符号个数(同上)
+14h    WORD          SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小
+16h    WORD          Characteristics;      // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  • Machine
    每个CPU都拥有唯一的Machine码,兼容32位Intel x86芯片的Machie码位14C。
  • NumberOfSections
    PE文件把代码、数据、资源等依据属性分类到各节区中存储,NumberOfSection用来指出文件中存在的节区数量,该值一定要大于0,且当定义的节区数与实际节区不同时,将发生运行错误。
  • TimeDataStamp
    用来记录编译器创建此文件的时间。
  • SizeOfOptionalHeader
    IMAGE_NT_HEADER结构体的最后一个成员位IMAGE_OPTIONAL_HEADER32结构体,SizeOfOptionalHeader成员用来指出IMAGE_OPTIONAL_HEADER32结构体的长度。
    PE32+格式的文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,而不是IMAGE_OPTIONAL_HEADER32结构体,2个结构体的尺寸是不同的,所以需要在SizeOfOptionalHeader成员明确指出结构体的大小。
  • Characteristics
    用于标识文件的属性,文件是否是可运行状态、是否位DLL文件等信息,以bit OR形式组合起来。
  • 查看IMAGE_FILE_HEADER结构体
    在这里插入图片描述
  1. NT头:可选头
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    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
       // 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  • Magic
    IMAGE_OPTIONAL_HEADER32结构体是,Magic码为10B;为IMAGE_OPTIONAL_HEADER64结构体时,Magic码为20B。
  • AddressOfEntryPoint
    持有EP的RVA值,指出程序最先执行的代码起始地址,相当重要。
  • ImageBase
    进程虚拟内存的范围时0~FFFFFFFF,PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装入地址。
    执行PE文件时,PE装载器先创建进程,再将文件装入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint.
  • SectionAlignment,FileAlignment
    PE文件的Body部分划分为若干节区,这些节区存储着不同类别的数据,FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment指定了节区在内存中的最小单位,磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍。
  • SizeOfImage
    加载PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占空间的大小,一般而言,文件的大小与加载到内存的大小是不同的。
  • SizeofHeader
    用来指出整个PE头的大小,该值也必须是FileAlignment的证书倍。
  • Subsystem
    用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe,*.dll).
  • NumberOfRvaAndSIzes
    NumberOfRvaAndSIzes用来指定DataDirectory数组(IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)的个数。
  • DataDirectory
    DataDirectory是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值。
    在这里插入图片描述
    重点关注EXPORT/IMPORT/RESOURCE、TLS Direction.

节区头

  1. PE格式的设计者决定把具有相似属性的数据统一保存在一个被称为节区的地方,然后需要把各节区属性记录在节区头中。
  2. 节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。
typedef struct _IMAGE_SECTION_HEADER 
{
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text” 
//IMAGE_SIZEOF_SHORT_NAME=8
union
+8h {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
+ch DWORD VirtualAddress; // 节区的 RVA 地址
+10h DWORD SizeOfRawData; // 在文件中对齐后的尺寸
+14h DWORD PointerToRawData; // 在文件中的偏移量
+18h DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
+1eh WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
+20h WORD NumberOfLinenumbers; // 行号表中行号的数目
+24h DWORD Characteristics; // 节属性如可读,可写,可执行等} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
}
  • VirtualSize:内存中节区所占的大小。
  • VirtualAddress:内存中节区起始地址。
  • SizeOfRawData:磁盘文件中节区所占大小。
  • PointerToRawData:磁盘文件中节区起始地址。
  • Characteristics:节区属性(bit OR)。

RAV to RAW

  1. PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移间的映射,这种映射一般称为RVA to RAW。
  • 查找RVA所在节区。
  • 使用简单的公式计算文件偏移。(RAW - PointerToRawData = RVA - VirtualAddress)。
    在这里插入图片描述
  1. EX:RVA = 5000, FILE Offset = 5000 - 1000 + 400 = 4400

IAT

  1. IAT是一种表格,用来记录程序正在使用哪些库中的哪些函数。
  2. DLL
  • 不要把库包含到程序中,单独组成DLL文件,需要时调用即可。
  • 内存映射技术使加载后的DLL代码、资源在多个进程中实现共享
  • 更新库时只要替换相关DLL文件即可,简便易行。
  1. 加载DLL的方式有两种
  • 一种是显式链接,程序使用DLL时加载,使用完毕后释放内存。
  • 另一种是隐式链接,程序开始时即一同加载DLL,程序终止时再释放占用的内存,IAT提供的机制与隐式链接有关。
  1. IMAGE_IMPORT_DESCRIPTOR
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // 包含指向IMAGE_DATA(输入名称表)RVA 的结构数组
    };
    DWORD   TimeDateStamp;                  //当可执行文件不与被导入的DLL进行绑定时,此字段为0
    DWORD   ForwarderChain;                 //第一个被转向的API索引
    DWORD   Name;                           //指向被导入的DLL 名称
    DWORD   FirstThunk;                     //指向输入地址表(IAT)RVA,IAT是一个IMAGE_THUNK_DATA结构的数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME {
	WORD Hint;
	BYTE Name[1];
}IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入哪些库文件,导入多少库就存在多个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体借宿。
  • IMAGE_IMPORT_DESCRIPTOR中的重要成员:
    在这里插入图片描述
  • 在PE文件没有导入到内存之前,OriginalFirstThunk和FirstThunk 结构都是一个东西,但是导入内存后,系统会根据OriginalFirstThunk对FirstThunk指向的表重新写入真实函数的地址。就是IAT了。
  1. 使用notepad.exe
  • IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即使IMAGE_IMPORT_DESCROPTOR结构体数组的起始地址,IMAGE_IMPORT_DESCRIPTOR结构体也被称为IMPORT Directory Table。
  • IMAGE_OPTIONAL_HEADER32.DataDirectory[1]结构体的值,第一个4字节为虚拟地址,第二个4字节为Size成员。
    在这里插入图片描述在这里插入图片描述
  • RVA是7604,故文件偏移为6A04(Raw = 7604 - 1000 + 400 = 6A04),定位到6A04.
    在这里插入图片描述
  • 阴影部分即为全部的IMAGE_IMPORT_DESCRIPTOR结构体数组,粗线框内的一部分是结构体数组的第一个元素。
    在这里插入图片描述
  • 库名称:Name是一个字符串指针,它指向导入函数所属的库文件名称,在文件偏移6EAC,可以看到comdlg32.dll。
    在这里插入图片描述
  • OriginalFirstThunk - INT : INT是一个包含导入函数信息的结构体指针数组,我们定位到6D90,每个地址分别指向IMAGE_IMPORT_BY_NAME结构体,跟踪数组的第一个值7A7A(RVA),进入该地址,可以看到导入的API函数的名称字符串。
    在这里插入图片描述
  • IMAGE_IMPORT_BY_NAME:
    RVA:7A7A -> RAW:6E7A。
    文件偏移6E7A最初的2个字节值为Ordinal,是库中函数的固有编号,Ordinal的后面为函数名称字符串PageSetupDlgW。
    如图,INT是IMAGE_IMPORT_BY_NAME结构体指针数组,数组的第一个元素指向函数Ordinal值为000F,函数的名称为PageSetupDlgW。
    在这里插入图片描述
  • FirstThunk - IAT
    IAT的RVA:12C4即为RAW:6C4.
    在这里插入图片描述
    文件偏移6C4~6EB区域即为IAT数组区域,对应与comdlg32.dll库,它与INT类似,由结构体指针数组组成,且以NULL结尾。
    IAT的第一个元素值被硬编码为76324906,该值无实际意义,notepad.exe文件加载到内存时,准备的地址值会取代该值。

EAT

  1. EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数,也就是说,只有通过EA才能准确求得从相应库中导出函数的起始地址。
  2. PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY结构体。
  3. 可以在PE文件的PE头中查找到IMAGE_EXPORT_DIRECTORY结构体的位置,IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress值即是IMAGE_EXPORT_DIRECTORY结构体数组的起始位置。
  4. 下图显示的是kernel32.dll文件的IMAGE_OPTIONAL_HEADER32.DataDirectory[0],第一个4字节为VirtualAddress,第二个4字节为Size成员。
    在这里插入图片描述
  • IMAGE_OPTIONAL_HEADER32.DataDirectory结构体数组信息整理如下表:
    在这里插入图片描述
  • 由于RVA的值为262C,所以文件偏移为1A2C.
  1. IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 未使用,总为0     DWORD   TimeDateStamp;      // 文件创建时间戳
    WORD    MajorVersion;       // 未使用,总为0     WORD    MinorVersion;       // 未使用,总为0
    DWORD   Name;               // 指向一个代表此 DLL名字的 ASCII字符串的 RVA
    DWORD   Base;               // 函数的起始序号
    DWORD   NumberOfFunctions;  // 导出函数的总数    DWORD   NumberOfNames;      // 以名称方式导出的函数的总数    DWORD   AddressOfFunctions;     // 指向输出函数地址的RVA
    DWORD   AddressOfNames;         // 指向输出函数名字的RVA
    DWORD   AddressOfNameOrdinals;  // 指向输出函数序号的RVA} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • NumberOfFuctions : 实际Export函数的个数。
  • NumberOfNames:Export函数中具名的函数个数。
  • AddressOfFunctions:Export函数地址数组
  • AddressOfFunctions:函数名称地址数组
  • AddressOfNameOrdinals: Ordinal地址数组
  • kernel32.dll文件的IMAGE_EXPORT_DIRECTORY结构体与整个EAT结构。
    在这里插入图片描述
  • GetProcAddress()函数用来从库中获得函数地址,该API引用引用EAT来获取指定API的地址,GetProcAddress()API拥有函数的名称:
    1. 利用AddressOfNames成员转到函数名称数组。
    2. 函数名称数组里面存储着字符串地址,通过比较字符串,查找指定的函数名称。(数组的索引称为name_index)
    3. 利用AddressOfNameOrdinals成员,转到orinal数组。
    4. 在ordinal数组通过name_index查找相应ordinal值。
    5. 利用AddressOfFunction成员转到函数地址数组。
    6. 在函数地址数组中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址。
  • kernel32.dll中所有导出函数均有相应名称,AddressOfNameOrdinals数组的值以index=ordianl存在,但并不是所有的DLL文件都如此,导出函数中也有一些函数没有名称,AddressOfNameOrdinals的index!=ordinal.
  1. 使用kernel32.dll练习
  • 从kernel32.dll文件的EAT查找AddAtomW函数,kernel32.dll的IMAGE_EXPORT_DIRECTORY结构体RAW为1A2C。
    在这里插入图片描述
  • 深色部分就是IMAGE_EXPORT_DIRECTORY结构体区域,IMAGE_EXPORT_DIRECTORY结构体的各个成员如图:
    在这里插入图片描述
    在这里插入图片描述
  • AddressOfName成员的值为RVA=353C,即RAW=293C,查看该地址
    在这里插入图片描述
  • 此处为4字节的RVA组成的数组,素组元素的个数为NumberOfNames(3BA),逐一跟随所有的RVA值即可发现函数名称字符串。
  • 要查找的函数名称字符串为“AddAtomW”,只要找到RVA数组第三个元素的值(RVA:4BBD -> RAW:3FBD)即可,进入地址就会看到“AddAtomW”字符串,此时“AddAtomW”函数名即使数组的第三个元素,数组索引为2。
    在这里插入图片描述
  • 查找AddAtomW函数的Ordinal值,AddressOfNameOrdinals成员的值为RVA:4424 -> RVA:3824,深色部分是多个2字节的ordinal组成的数组。
    在这里插入图片描述
  • 之前求得的name_index值为2,根据name_index的值计算Ordinal数组的值即可求得AddressOfNameOrdinals[2] = 2.
  • 最后查找AddAtomW的实际函数地址,AddressOfFunctions成员的值为RVA:2654 -> RAW:1A54,深色部分即为4字节函数地址RVA数组,它就是Export函数的地址。
    在这里插入图片描述
  • 为了获取AddAtomW函数的地址,将求得的Ordianl用作数组的索引,得到RVA=00326F1 -> AddressOfFunctions[ordinal] = RVA(ordinal=2,RVA=326F1)
  • kernel32.dll的ImageBase=7C7D000,因此AddAtomW函数的实际地址(VA)为7C8026F1(7C7D0000 + 326F1 = 7C8026F1).
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值