锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

PE文件结构详解 --(完整版)

时间:2022-09-21 15:30:00 q18j5a连接器

From:https://blog.csdn.net/adam001521/article/details/84658708

PE结构详解:https://www.cnblogs.com/zheh/p/4008268.html

PE格式分析-区段表及导入表结构详解:https://blog.csdn.net/qq_30145355/article/details/78859214

PE文件基本分析:https://www.52pojie.cn/thread-1023342-1-1.html

PE导出表/导入表在结构体中分析-初级:https://bbs.pediy.com/thread-224265.htm

PE 文件格式分析:https://blog.csdn.net/as14569852/article/details/78120335

小甲鱼PE结构详解:https://www.bilibili.com/video/av6861026

木马,免杀,病毒都需要了解PE结构

PE文件结构超详图:

图 1:

图 2:

图 3:

(一)基本概念

下表描述了贯穿本文的一些概念:

名称 描述
地址 是 “虚拟地址” 而不是“物理地址”。为什么不是物理地址?由于数据在内存中的位置经常发生变化,因此可以节省内存费用,避免错误的内存位置。同时,用户不需要知道具体的真实地址
镜像文件 包含以 EXE 以文件为代表 可执行文件,以DLL以文件为代表“动态链接库”。为什么用“镜像”?这是因为他们常常被直接“复制”到内存,有“镜像”的某种意思。
RVA 英文全称 Relatively Virtual Address。偏移(又称相对虚拟地址)。镜像基址的偏移。
是 PE 文件中 代码数据 基本单元。原则上,节只分为 “代码节” 和 “数据节” 。
VA 英文全称 Virtual Address。基址

PE( Portable Execute)文件是Windows以下可执行文件总称,常见的有DLL,EXE,OCX,SYS等等。它在微软 UNIX 平台的 COFF在通用对象文件格式的基础上制作而成。最初的设计用于提高程序在不同操作系统中的移植性,但实际上这种文件格式只用于 Windows 在系列操作系统下PE文件是指 32 位可执行文件又称PE32.64位可执行文件称为 PE 或 PE32 ,是PE(PE32)一种扩展形式(请注意不是PE64)

事实上,是否有文件 PE 文件与其扩展名无关,PE文件可以是任何扩展名。 Windows 如何区分可执行文件和非可执行文件?我们调用 LoadLibrary 传递文件名,该系统如何判断该文件是一个合法的动态库?这涉及到PE文件结构。

PE一般来说,文件的结构如下图所示:从起始位置依次 DOS头NT头节表 以及 具体的节

PE 执行文件的顺序

  1. 当一个 PE 文件 被执行时,PE 装载器 首先检查 DOS header 里的 PE header 偏移量。如果发现,直接跳转到 PE header 的位置。
  2. 当 PE装载器 跳转到 PE header 之后,第二步是检查 PE header 是否有效。如果是这样 PE header 有效,跳转 PE header 的尾部。
  3. 紧跟 PE header 节表在尾部。PE装载机执行第二步后,开始读取节表中的节段信息,并使用文件映射( 在执行一个PE文件时,Windows整个文件不是从一开始就读入内存,而是采用与内存映射的机制,即,Windows装载时,装载器只建立虚拟地址和PE只有当文件之间的映射关系真正执行到内存页面的指令或访问页面中的数据时,页面才会从磁盘提交到物理内存,这种机制使得文件装入的速度与文件的大小无关 )将这些节段映射到内存的方法,同时,附上节表中指定节段的读写属性
  4. PE文件映射到内存后,PE装载器将继续处理PE文件中类似 import table (输入表)逻辑部分。

这四步是PE读者可以参考文件的执行顺序。(以上四个步骤摘自以上四个步骤。《黑客破解精通》)

PE文件结构说明:

  1. DOS头 是用来兼容 MS-DOS 操作系统的,目的是当这个文件在 MS-DOS 大多数情况下,上操作时提示一段文字:This program cannot be run in DOS mode. 另一个目的是指明 NT 头在文件中。
  2. NT头 包含 windows PE 文件的主要信息包括一个'PE'字样的签名PE文件头(IMAGE_FILE_HEADER)PE可选头(IMAGE_OPTIONAL_HEADER32)。
  3. 节表:是 PE 文件后续节的描述,windows 根据节表的描述加载每个节。
  4. :每个节实际上是一个容器,可以包含 代码、数据 等等,每个节都可以有独立的内存权限,比如默认的代码节有读/执行权限,节的名称和数量可以自己定义,不一定是上图中的三个。

相对虚拟地址RVA虚拟地址VA

当一个 PE 文件加载到内存后,我们称之为 "映象 "(image),一般来说,PE硬盘上的文件与内存不完全相同加载到内存后占用的虚拟地址空间比硬盘上占用的空间要大这是因为每个节在硬盘上是连续的,在内存中是按页对齐的,所以加载到内存后节之间会有一些 “空洞” 。

因为存在这种对齐,所以在 PE 在结构内部,有两种方式表示某个位置的地址:

  1. 存储在硬盘上的文件中的地址称为 原始存储地址物理地址表示 偏离文件头。
  2. 加载到内存后图像中的地址称为相对虚拟地址(RVA),表示相对内存图像头的偏移

堆栈:堆栈中的数据是临时存储的数据临时存储位置,如参数、局部变量和计算中间值使用数据时,使用pop出栈使用数据,清除堆栈上的相应数据。

然而 CPU 有些指令需要使用绝对地址,如取全局变量地址、传输函数地址、编译后的汇编指令必须使用绝对地址而不是相对图像头偏移,因此 PE 建议操作系统其加载到某个内存地址(这个叫基地址。段地址其实就是一种基地址,但基地址并不等于就是段地址。所谓基地址 ( 可以理解为汇编中全局变量 ),顾名思义就可以理解为基本地址,他是相对偏移量的计算基准,即参考位置。基地址和偏移地址的概念:https://blog.51cto.com/godben/1746144,基地址 可以理解为 内存中整个 PE 文件的头地址,文件最开始的位置),编译器便根据这个地址求出代码中一些 全局变量 和 函数的地址,并将这些地址用到对应的指令中。例如在 IDA 里看上去是这个样子:

这种表示方式叫做 虚拟地址(VA)。

RVA 和文件偏移换算

https://www.bilibili.com/video/av28047648/?p=7

也许有人要问,既然有 VA 这么简单的表示方式为什么还要有前面的 RVA 呢?因为虽然PE文件虽然为自己指定加载的基地址,但是 windows 有茫茫多的 DLL,而且每个软件也有自己的 DLL,如果指定的地址已经被别的 DLL 占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的所以在 PE 文件头中大部分是使用 RVA 来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后 VA 都失效了,那存在于代码中的那些 VA 怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么 NT 头不能依靠重定位采用 VA 表示地址呢(十万个为什么)?因为不是所有的 PE 都有重定位,早期的 EXE 就是没有重定位的。

我们都知道 PE 文件可以导出函数让其他的 PE 文件使用,也可以从其他 PE 文件导入函数,这些是如何做到的?PE 文件通过 导出表 指明自己导出那些函数,通过 导入表 指明需要从哪些模块导入哪些函数。导入表导出表 的具体结构会在单独的文章中详细解释。

PE文件内存映像(映射)

小甲鱼视频:https://www.bilibili.com/video/av28047648/?p=4

就是把 PE 文件 从 硬盘中 放到 内存中,然后 CPU 从 内存中读取指令并执行。 

文件中使用偏移(offset),内存中使用 VA(Virtual Address,虚拟地址)来表示位置。

VA 指进程虚拟内存的绝对地址RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(ImageBase)开始的相对地址。VA 与 RVA 满足下面的换算关系: RVA + ImageBase = VA

***************************************************************
PE 头内部信息大多是 RVA 形式存在。
原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的 PE文件(DLL)。
此时必须通过重定向(Relocation)将其加载到其他空白的位置,若 PE头信息使用的是 VA,则无法正常访问。
因此使用 RVA 来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
***************************************************************

当 PE 文件被执行时,PE 装载器会为 进程 分配 4GB 的 虚拟地址空间( Virtual address spaces 官方文档:https://docs.microsoft.com/zh-tw/windows-hardware/drivers/gettingstarted/virtual-address-spaces ),然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到 虚拟地址空间 中的 0X400000的位置。

扩展:虚拟地址空间

浅谈进程地址空间与虚拟存储空间:https://www.cnblogs.com/fengliu-/p/9243004.html
Linux内存管理 4---虚拟地址空间管理:https://www.cnblogs.com/smartjourneys/p/7196868.html
进程的虚拟地址空间分布:http://www.mamicode.com/info-detail-2487121.html
虚拟地址空间:https://blog.csdn.net/qq_33883085/article/details/88430087
Linux 虚拟地址空间:https://blog.csdn.net/Jocker_D/article/details/83659465

  • 虚拟内存:windows下的 虚拟内存 指的是在硬盘上建一个文件,用来放置系统非活跃性内存数据或交换数据 ( 怎么放,放多少由操作系统决定)。虚拟内存通常只在系统物理内存用完时,才会使用到,但这个时候系统已经非常卡了。但也不是一点用处没有,非活跃性进程的部分数据,系统是完全可以放在虚拟内存中的。
  • 虚拟地址空间:指 windows下 每个进程的私有内存空间,大小是4G,能访问的是不到2G的空间,其余是系统保留的。这2G是能访问的,但并不是立即分配的,当进程使用多少时,才从物理内存中划分给它多少,划分的的方式是 "映射",操作系统将虚拟内存的起始地址做个标记,标记成对应的物理内存的某个地址上。在这里,只有操作系统知道,进程是没有任何办法知道的,这是 WINDOWS 的高级内存管理机制决定的。物理内存的地址空间,只有操作系统才能访问(硬件驱动也可以,但已经属于系统低层了,进程是属于用户层 ) 。进程 虚拟内存空间物理内存空间 的关系仅仅是看不见的映射关系.

( 虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是 虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户模式空间(user mode space)两部分组成。虚拟地址会通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用,每个进程都有自己的页表。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页是会导致一个页错误(page fault)。其中内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。与此相反,用户模式空间的映射随进程切换的发生而不断变化。)

  • 程序一旦被执行就成为一个 进程,内核就会为每个运行的进程提供了大小相同的虚拟地址空间,这使得多个进程可以同时运行而又不会互相干扰
  • 具体来说一个进程对某个地址的访问,绝不会干扰其他进程对同一地址的访问。
  • 每个进程都拥有4GB(32位)大小的虚拟地址空间每个进程都拥有私有的前3G空间,即“用户空间”而后1G空间被每个进程所共享,即“内核空间”。
  • 进程 访问内核空间 的唯一途径为 系统调用
  • 在每个进程眼中,它们各自拥有4GB大小的地址空间;而在CPU眼中,任意时刻,一个CPU上只存在一个虚拟地址空间。虚拟地址空间随着进程间的切换而变化。

分析一个完整的程序

DOS 头(分 Header 和 DOS 存根)

Header 结构 (00000000 - 0000003F,共 64 个字节)

注意 Win+Intel 的电脑上大部分采用 ”小端法”,字节在内存中储存方式是倒过来的。

重要参数为 e_magic 和 e_lfanew ,已用不同颜色体现。

DOS存根(00000040 - 000000BF,共128字节)

DOS存根则是一段简单的DOS程序,主要用来输出类似“This program cannot be run in DOS mode.”的提示语句。即使没有DOS存根,程序也能正常执行。参考:DOS存根写入数据的想法

NT头(PE最重要的头,由 DOS 头中的 e_lfanew 决定 ,表示 DOS头之后的 NT头相对文件起始地址的偏移)

  • IMAGE_NT_HEADERS32

  • IMAGE_FILE_HEADER:其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。

typedef struct _IMAGE_FILE_HEADER { 
        WORD    Machine;              '// 每个CPU拥有唯一的Machine码 -> 4C 01 -> PE -> 兼容32位Intel X86芯片'

        WORD    NumberOfSections;     '// 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 04 -> 4
                                       // 该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。'

        DWORD   TimeDateStamp;         // PE文件的创建时间,一般有连接器填写 -> 38 D1 29 1E
        DWORD   PointerToSymbolTable;  // COFF文件符号表在文件中的偏移 -> 00 00 00 00
        DWORD   NumberOfSymbols;       // 符号表的数量 -> 00 00 00 00

        WORD    SizeOfOptionalHeader; '// 指出IMAGE_OPTIONAL_HEADER32结构体的长度。->  00 E0 -> 224字节
                                       // PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
                                       // 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。'

        WORD    Characteristics;      '// 标识文件的属性,二进制中每一位代表不同属性 -> 0F 01
                                       // 属性参见https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.2'
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

图解:

  • IMAGE_OPTIONAL_HEADER:其中有9个重要参数,设置错误会导致文件无法运行

小甲鱼视频地址 IMAGE_OPTIONAL_HEADER 讲解:bilibili.com/video/av28047648/?p=3​​​

typedef struct _IMAGE_OPTIONAL_HEADER { 
        WORD    Magic;                     '// 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
        BYTE    MajorLinkerVersion;         // 链接器的主版本号 -> 05
        BYTE    MinorLinkerVersion;         // 链接器的次版本号 -> 0C
        DWORD   SizeOfCode;                 // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 40 00 04 00
        DWORD   SizeOfInitializedData;      // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 40 00 0A 00
        DWORD   SizeOfUninitializedData;    // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
        DWORD   AddressOfEntryPoint;       '// 指出程序最先执行的代码起始地址(RVA) -> 00 00 10 00'
        DWORD   BaseOfCode;                 // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 10 00

        DWORD   BaseOfData;                 // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 20 00
                                            // 在64位文件中此处被并入紧随其后的ImageBase中。

        DWORD   ImageBase;                 '// 当加载进内存时,镜像的第1个字节的首选地址。
                                            // WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
                                            // 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
                                            // 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint'

                                           '// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。'
        DWORD   SectionAlignment;          '// SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00'
        DWORD   FileAlignment;             '// FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
                                            // SectionAlignment必须大于或者等于FileAlignment'

        WORD    MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 04
        WORD    MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
        WORD    MajorImageVersion;          // 镜像的主版本号 -> 00 00
        WORD    MinorImageVersion;          // 镜像的次版本号 -> 00 00
        WORD    MajorSubsystemVersion;      // 子系统的主版本号 -> 00 04
        WORD    MinorSubsystemVersion;      // 子系统的次版本号 -> 00 00
        DWORD   Win32VersionValue;          // 保留,必须为0 -> 00 00 00 00

        DWORD   SizeOfImage;               '// 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
                                            // 一般文件大小与加载到内存中的大小是不同的。 -> 00 00 50 00'

        DWORD   SizeOfHeaders;             '// 所有头的总大小,向上舍入为FileAlignment的倍数。
                                            // 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00'

        DWORD   CheckSum;                   // 镜像文件的校验和 -> 00 00 B4 99

        WORD    Subsystem;                 '// 运行此镜像所需的子系统 -> 00 02 -> 窗口应用程序
                                            // 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll),
                                            // 参考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'

        WORD    DllCharacteristics;         // DLL标识 -> 00 00
        DWORD   SizeOfStackReserve;         // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
        DWORD   SizeOfStackCommit;          // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
        DWORD   SizeOfHeapReserve;          // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
        DWORD   SizeOfHeapCommit;           // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
        DWORD   LoaderFlags;                // 保留,必须为0 -> 00 00 00 00

        DWORD   NumberOfRvaAndSizes;       '// 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10'
        IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 数据目录数组。详见下文。' 
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

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

DataDirectory[] 数据目录数组:数组每项都有被定义的值,不同项对应不同数据结构。重点关注的 IMPORT 和 EXPORT,它们是 PE 头中的非常重要的部分,其它部分不怎么重要,大致了解下即可。

'#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory '
'#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory '
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory 
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory 
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory 
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table 
#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

(二)可执行文件头

在 PE 文件结构详解(一)基本概念里,解释了一些 PE 文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构。

了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格式,重要的信息都包含在头部,顺着头部的信息,可以引导系统解析整个文件。所以,我们先来认识一下PE文件的头部格式。还记得上篇里的那个图吗?

DOS头NT头 就是 PE 文件中两个重要的文件头。

一、DOS头

DOS头 的作用是兼容 MS-DOS 操作系统中的可执行文件,对于 32位PE文件来说,DOS 所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在 DOS 上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

我们只需要关注两个域:

  1. e_magic:一个 WORD 类型,值是一个常数 0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是'MZ'开头。
  2. e_lfanew:为 32 位可执行文件扩展的域,用来表示 DOS头之后的 NT头相对文件起始地址的偏移

二、NT头

顺着 DOS 头中的 e_lfanew,我们很容易可以找到 NT头,这个才是 32位PE文件中最有用的头,定义如下:

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

下图是一张真实的 PE文件头结构 以及其 各个域的取值 

块对齐:

Signature 签名 ):

类似于 DOS头中的 e_magic,其高16位是0,低16是0x4550,用字符表示是 'PE‘ 。

IMAGE_FILE_HEADER是PE文件头其中在C语言的定义是这样的

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;

每个域的具体含义如下:

Machine:该文件的运行平台,是 x86、x64 还是 I64 等等,可以是下面值里的某一个。

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

NumberOfSections:      该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp:           PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable: COFF文件符号表在文件中的偏移。
NumberOfSymbols:      符号表的数量。
SizeOfOptionalHeader: 紧随其后的可选头的大小。
Characteristics:             可执行文件的属性,可以是下面这些值按位相或。

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

可以看出,PE 文件头定义了 PE 文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。

PE 可选头别看名字叫可选头,其实一点都不能少

另一个重要的头就是 PE 可选头,别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。

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 可选头 各个字段分析说明

  • Magic:表示可选头的类型。
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b  // 32位PE可选头
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b  // 64位PE可选头
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107
  • MajorLinkerVersion MinorLinkerVersion:链接器的版本号。
  • SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
  • SizeOfInitializedData:初始化的数据长度。
  • SizeOfUninitializedData:未初始化的数据长度。
  • AddressOfEntryPoint:程序入口的 RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。
  • BaseOfCode:代码段起始地址的RVA。
  • BaseOfData:数据段起始地址的RVA。
  • ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
  • SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
  • FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
  • MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
  • MajorImageVersionMinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
  • MajorSubsystemVersionMinorSubsystemVersion:所需子系统版本号。
  • Win32VersionValue:保留,必须为0。
  • SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
  • SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
  • CheckSum:映象文件的校验和。
  • Subsystem:运行该PE文件所需的子系统,可以是下面定义中的某一个:
#define IMAGE_SUBSYSTEM_UNKNOWN              0   // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE               1   // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI              5   // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI            7   // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION      10  //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER  11   //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER   12  //
#define IMAGE_SUBSYSTEM_EFI_ROM              13
#define IMAGE_SUBSYSTEM_XBOX                 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040     // DLL can move.
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY    0x0080     // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT    0x0100     // Image is NX compatible
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200     // Image understands isolation and doesn't want it
#define IMAGE_DLLCHARACTERISTICS_NO_SEH       0x0400     // Image does not use SEH.  No SE handler may reside in this image
#define IMAGE_DLLCHARACTERISTICS_NO_BIND      0x0800     // Do not bind this image.
//                                            0x1000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER   0x2000     // Driver uses WDM model
//                                            0x4000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE     0x8000
  • SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
  • SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
  • SizeOfHeapReserve:运行时为进程堆保留内存大小。
  • SizeOfHeapCommit:运行时进程堆初始占用内存大小。
  • LoaderFlags:保留,必须为0。
  • NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
  • DataDirectory:数据目录,这是一个数组,数组的项定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  • VirtualAddress:是一个RVA。
  • Size:是一个大小。

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

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#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

看到这么多的定义,大家估计要头疼了,好不容易要把 PE文件头学习完了,又 “从天而降” 一大波的结构。不用紧张,有了前面的知识,后面的部分就迎刃而解了。下一篇开始将沿着这个数据目录分解其余部分,继续关注哦~

小甲鱼 区块表 和 区块:https://www.bilibili.com/video/av28047648/?p=5

小甲鱼 区块描述及意义:https://www.bilibili.com/video/av28047648/?p=6

(三)PE导出表

小甲鱼视频 - 导出表 :https://www.bilibili.com/video/av28047648/?p=10

导出表 是 用来描述 模块(dll)中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。导出表 就是一个 " 表格 "

扩展名为 .exe 不存在导出表,导出表 存在在 dll 文件中,就是 导出 函数给别人用。

上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数,DataDirectory中的每一项都可以用这个函数获取,函数原型如下:

PVOID NTAPI RtlImageDirectoryEntryToData(PVOID Base, BOOLEAN MappedAsImage, USHORT Directory, PULONG Size);
        Base:模块基地址。
        MappedAsImage:是否映射为映象。
        Directory:数据目录项的索引。

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#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

        Size:对应数据目录项的大小,比如Directory为0,则表示导出表的大小。

返回值表示数据目录项的起始地址。

这次来看看第一项:导出表。

导出表 是 用来描述 模块(dll)中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址

函数导出的方式有两种:

  1. 一种是 按 名字 导出,
  2. 一种是 按 序号 导出。

这两种导出方式在导出表中的描述方式也不相同。

模块的导出函数可以通过Dependency walker工具来查看:

上图中红框位置显示的就是模块的导出函数,有时候显示的导出函数名字中有一些符号,像 ??0CP2PDownloadUIInterface@@QAE@ABV0@@Z,这种是导出了C++的函数名,编译器将名字进行了修饰。

下面看一下导出表的定义吧:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

导出表结构:

结构还算比较简单,具体每一项的含义如下:

  1. Characteristics:现在没有用到,一般为0。
  2. TimeDateStamp:导出表生成的时间戳,由连接器生成。
  3. MajorVersion,MinorVersion:看名字是版本,实际貌似没有用,都是0。
  4. Name:模块的名字。
  5. Base:序号的基数,按序号导出函数的序号值从Base开始递增。
  6. NumberOfFunctions:所有导出函数的数量。
  7. NumberOfNames:按名字导出函数的数量。
  8. AddressOfFunctions:一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。
  9. AddressOfNames:一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。
  10. AddressOfNameOrdinals:一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

第一次接触这个结构的童鞋被后面的5项搞晕了吧,理解这个结构比结构本身看上去要复杂一些,文字描述不管怎么说都显得晦涩,所谓一图胜千言,无图无真相,直接上图:

在上图中,AddressOfNames 指向一个数组,数组里保存着一组 RVA,每个RVA指向一个字符串,这个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的对应项。获取导出函数地址时,先在AddressOfNames中找到对应的名字,比如Func2,他在AddressOfNames中是第二项,然后从AddressOfNameOrdinals中取出第二项的值,这里是2,表示函数入口保存在AddressOfFunctions这个数组中下标为2的项里,即第三项,取出其中的值,加上模块基地址便是导出函数的地址。如果函数是以序号导出的,那么查找的时候直接用序号减去Base,得到的值就是函数在AddressOfFunctions中的下标。

用代码实现如下:

DWORD* CEAT::SearchEAT( const char* szName)
{
    if (IS_VALID_PTR(m_pTable))
    {
        bool bByOrdinal = HIWORD(szName) == 0;
        DWORD* pProcs = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfFunctions));
        if (bByOrdinal)
        {
            DWORD dwOrdinal = (DWORD)szName; 
            if (dwOrdinal < m_pTable->NumberOfFunctions && dwOrdinal >= m_pTable->Base)
            {
                return &pProcs[dwOrdinal-m_pTable->Base];
            }
        }
        else
        {
            WORD* pOrdinals = (WORD*)((char*)RVA2VA(m_pTable->AddressOfNameOrdinals));
            DWORD* pNames = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfNames));
            for (unsigned int i=0; iNumberOfNames; ++i)
            {
                char* pNameVA = (char*)RVA2VA(pNames[i]);
                if (strcmp(szName, pNameVA) != 0)
                {
                    continue;
                }
                return &pProcs[pOrdinals[i]];
            }
        }
    }
    return NULL;
}

(四)PE导入表

小甲鱼视频 - 导入表:https://www.bilibili.com/video/av28047648/?p=8

https://www.bilibili.com/video/av28047648/?p=9

.exe 文件存在导入表,就是导入函数,然后自己使用。

导入表 在 PE 文件加载时,会根据这个表里的内容加载依赖的 DLL ( 模块 ),并填充所需函数的地址。

C++ 代码:

#include 

int WINAPI WinMain(
	HINSTANCE hInstance, 
	HINSTANCE hPrevInstance, 
	PSTR szCmdLine, 
	int iCmdShow
)
{
	MessageBox(
		NULL, 
		TEXT("hello, welcome to fichc.com"), 
		TEXT("welcome"), 
		MB_OKCANCEL | MB_OK
	);
	return 0;
}

PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPORT,即导入表。

也许大家注意到过,在 IMAGE_DATA_DIRECTORY 中,有几项的名字都和导入表有关系,其中包括:IMAGE_DIRECTORY_ENTRY_IMPORTIMAGE_DIRECTORY_ENTRY_BOUND_IMPORTIMAGE_DIRECTORY_ENTRY_IAT IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 这几个导入都是用来干什么的,他们之间又是什么关系呢?听我慢慢道来。

  1. IMAGE_DIRECTORY_ENTRY_IMPORT 就是我们通常所知道的 导入表在 PE 文件加载时,会根据这个表里的内容加载依赖的 DLL ( 模块 ),并填充所需函数的地址。
  2. IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 叫做 绑定导入表,在第一种导入表导入地址的修正是在PE加载时完成,如果一个PE文件导入的DLL或者函数多那么加载起来就会略显的慢一些,所以出现了绑定导入,在加载以前就修正了导入表,这样就会快一些。
  3. IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 叫做 延迟导入表,一个PE文件也许提供了很多功能,也导入了很多其他DLL,但是并非每次加载都会用到它提供的所有功能,也不一定会用到它需要导入的所有DLL,因此延迟导入就出现了,只有在一个PE文件真正用到需要的DLL,这个DLL才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。
  4. IMAGE_DIRECTORY_ENTRY_IAT 导入地址表,前面的三个表其实是导入函数的描述,真正的函数地址是被填充在导入地址表中的。

举个实际的例子,看一下下面这张图:

这个代码调用了一个 RegOpenKeyW 的导入函数,我们看到其 opcode 是 FF 15 00 00 19 30,其实 FF 15 表示这是一个间接调用,即 call dword ptr [30190000] ,这表示要调用的地址存放在 30190000 这个地址中,而 30190000 这个地址在导入地址表的范围内,当模块加载时,PE 加载器会根据导入表中描述的信息修正30190000这个内存中的内容。

那么导入表里到底记录了那些信息,如何根据这些信息修正 IAT ( 导入地址表 ) 呢?我们一起来看一下导入表的定义:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    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;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

使用 RtlImageDirectoryEntryToData 并将索引号传 1,会得到一个如上结构的指针,实际上指向一个上述结构的数组,每个导入的 DLL 都会成为数组中的一项,也就是说,一个这样的结构对应一个导入的 DLL。

  • Characteristics OriginalFirstThunk:一个联合体,如果是数组的最后一项 Characteristics 为 0,否则 OriginalFirstThunk 保存一个 RVA,指向一个 IMAGE_THUNK_DATA 的数组,这个数组中的每一项表示一个导入函数。
  • TimeDateStamp:映象绑定前,这个值是0,绑定后是导入模块的时间戳。
  • ForwarderChain:转发链,如果没有转发器,这个值是 -1 。
  • Name:一个 RVA,指向导入模块的名字,所以一个 IMAGE_IMPORT_DESCRIPTOR 描述一个导入的DLL。
  • FirstThunk:也是一个 RVA,也指向一个 IMAGE_THUNK_DATA 数组。

既然 OriginalFirstThunk 与 FirstThunk 都指向一个 IMAGE_THUNK_DATA 数组,而且这两个域的名字都长得很像,他俩有什么区别呢?为了解答这个问题,先来认识一下 IMAGE_THUNK_DATA 结构:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

ForwarderString 是转发用的,暂时不用考虑,Function 表示函数地址,如果是按序号导入 Ordinal 就有用了,若是按名字导入AddressOfData 便指向名字信息。可以看出这个结构体就是一个大的union,大家都知道union虽包含多个域但是在不同时刻代表不同的意义

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章