13.PE文件格式

1. PE文件格式介绍

PE文件是Windows操作系统下使用的可执行文件格式。它是微软在UNIX平台的COFF(Common Object File Format),通用对象文件格式)基础上制作而成的。最初(正如Portable这个单词所代表的那样)设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列的操作系统下。
PE文件是指32位的可执行文件,也称为PE3264位的可执行文件称为PE+或PE32+,是PE(PE32)文件的一种扩展形式(请注意不是PE64)。 

2. PE文件格式

种类 主拓展名
可执行系列 exe、scr
驱动程序系列 sys、vxd
库系列 dll、ocx、cpl、drv
对象文件系列 obj

严格地说,OBJ(对象)文件之外的所有文件都是可执行的。DLL、SYS文件等虽然不能直接在Shell(Explorer.exe)中运行,但可以使用其他方法(调试器、服务等)执行。

根据PE正式规范,编译结果OBJ文件也视为PE文件。但是OBJ文件本身不能以任何形式执行,在代码逆向分析中几乎不需要关注它

然后我们用010打开记事本,
图中是 notepad.exe 文件的起始部分,也是PE文件的头部分(PEheader)。notepad.exe文件运行需要的所有信息就存储在这个PE头中。如何加载到内存、从何处开始运行、运行中需要的DLL有哪些、需要多大的栈/堆内存等,大量信息以结构体形式存储在PE头中。
换言之,学习PE文件格式就是学习PE头中的结构体
Pasted image 20250313130611

2.1. 基本结构

从DOS头(DOSheader)到节区头(Section header)是PE头部分,其下的节区合称PE体。文件中使用偏移(offset),内存中使用VA(VirtualAddress,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区的大小、位置等)。

文件的内容一般可分为代码(.text)、数据(.data)、资源(.rsrc)节,分别保存。

根据所用的不同开发工具(VB/VC++/Delphi/etc)与编译选项,节区的名称、大小、个数、存储的内容等都是不同的。最重要的是它们按照不同的用途分类保存到不同的节中。

Pasted image 20250313132029

各节区头定义了各节区在文件或内存中的大小、位置、属性等。

PE头与各节区的尾部存在一个区域,称为NULL填充(NULLpadding)。计算机中,为了提高处理文件、内存、网络包的效率,使用“最小基本单位”这一概念,

PE文件中也类似。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数位置上,空白区域将用NULL填充(看图,可以看到各节区起始地址的截断都遵循一定规则)。

2.2. VA&RVA

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

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

32位WindowsOS中,各进程分配有4GB的虚拟内存,因此进程中VA值的范围是00000000~FFFFFFFF
64位是 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF(16EB)

3. PE头

PE头由许多结构体组成

3.1. DOS头

微软创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑了PE文件对DOS文件的兼容性。其结果是在PE头的最前面添加了一个 IMAGE_DOS_HEADER 结构体,用来扩展已有的DOS EXE头。
如图
Pasted image 20250313132930
IMAGE_DOS_HEADER 结构体的大小为 0x40个字节。 主要由两个重要成员

  • e_magic:DOS签名(signature,4D5A=>ASCI值“MZ”)。
  • e_lfanew:指示NT头的偏移(根据不同文件拥有可变值)。
    Pasted image 20250313133433

    这里的NT头偏移是 000000E0,因为Intel系列的CPU以逆序存储数据,这称为小端序标识法

如果我们修改这两处任意一个值,都会发现程序无法运行(因为根据PE规范,它已经不是PE文件了)
Pasted image 20250313134056

3.2. DOS存根

DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(即使没有DOS存根,文件也能正常运行)。DOS存根由代码与数据混合而成
Pasted image 20250313134207
其中40-4D处的16进制是一串16位的汇编指令,32位的windowsOS中不会运行此命令(由于被识别为PE文件,所以完全忽视该代码)
如果在DOS环境中运行记事本,或者使用调试器运行,就会输出 This program cannot be run in DOS mode 然后就退出

3.3. NT头

NT头 IMAGE_NT_HEADERS 结构体由3个成员组成

  • 签名(Signature)结构体,其值为50450000h(“PE”00)
  • 文件头(FileHeader)结构体
  • 可选头(OptionalHeader)结构体
    Pasted image 20250313135141

    32位NT头结构体大小为 0xF8 248字节 (4 + 20 + 224)
    64位NT头结构体大小为0x108 264字节(4 + 20 + 240)

3.3.1. NT头:文件头

Pasted image 20250313143804
文件头 IMAGE_FILE_HEADERS 结构体中有如下4种重要成员(若它们设置不正确,将导致文件无法正常运行)。

1.Machine
每个CPU都拥有唯一的Machine码,兼容32位Intelx86芯片的Machine码为14C。
Pasted image 20250313135749
Pasted image 20250313135833

2.NumberOfSections
前面提到过,PE文件把代码、数据、资源等依据属性分类到各节区中存储。
NumberOfSections 用来指出文件中存在的节区数量。该值一定要大于0,且当定义的节区数量与实际节区不同时,将发生运行错误。

3 .SizeOfOptionalHeader
IMAGE_NT_HEADER结构体的最后一个成员为IMAGE_OPTIONAL_HEADER32结构体。
SizeOfOptionalHeader成员用来指出IMAGE_OPTIONAL_HEADER32结构体的长度

IMAGE_OPTIONAL_HEADER32 结构体由C语言编写而成,故其大小已经确定。但是Windows的PE装载器需要查看 IMAGE_FILE_HEADERSizeOfOptionalHeader 值,从而识别出 IMAGE_OPTIONAL_HEADER32 结构体的大小。

PE32+格式的文件中使用的是 IMAGE_OPTIONAL_HEADER64 结构体,而不是 IMAGEOPTIONAL_HEADER32 结构体。2个结构体的尺寸是不同的,所以需要 SizeOfOptionalHeader 成员中明确指出结构体的大小。

借助 IMAGE_DOS_HEADERe_lfanew 成员与 IMAGE_FILE_HEADERSizeOfOptionalHeader 成员,可以创建出一种脱离常规的PE文件(PEPatch)(也有人称之为“麻花”PE文件)。

4.CHaracteristics
该字段用于标识文件的属性,文件是否是可运行的形态、是否为DLL文件等信息,以bitOR形式组合起来。
以下是定义在 winnt.h 文件中的Characteristics值(请记住0002h与2000h这两个值)
Pasted image 20250313140519

另外,PE文件中Characteristics的值可以不是0002h(不可执行的),比如类似*.obj的object文件及resourceDLL文件等。

5. 不影响文件运行的 TimeDateStampA 成员
该成员的值不影响文件运行,用来记录编译器创建此文件的时间。但是有些开发工具(VB、VC++)提供了设置该值的工具,而有些开发工具(Delphi)则未提供(且随所用选项的不同而不同)
Pasted image 20250313140846

3.3.2. NT头:可选 头

Pasted image 20250313143733
IMAGE_OPTIONAL_HEADER32 是PE头结构体中最大的。
IMAGE_OPTIONAL_HEADER32 结构体中需要关注下列成员。这些值是文件运行必需的,设置错误将导致文件无法正常运行。

1.Magic
IMAGE_OPTIONAL_HEADER32 结构体时,Magic码为 10B
IMAGE_OPTIONALHEADER64 结构体时,Magic码为 20B

2 .AddressOfEntryPoint
AddressOfEntryPoint持有EP的RVA值。该值指出程序最先执行的代码起始地址,相当重要。

3 .ImageBase
进程虚拟内存的范围是0~FFFFFFFF(32位系统)。PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装人地址。

EXE、DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被载人内核内存的80000000~FFFFFFFF中。一般而言,使用开发工具(VB/VC++/Delphi)创建好EXE文件后,其ImageBase的值为00400000,DLL文件的ImageBase值为10000000(当然也可以指定为其他值)。执行PE文件时,PE装载器先创建进程,再将文件载人内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint。

4. SectionAlignment, FileAlignment
PE文件的Body部分划分为若干节区,这些节存储着不同类别的数据。FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位(一个文件中,FileAlignment与SectionAlignment的值可能相同,也可能不同)。
磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍。

5 . SizeOfImage
加载PE文件到内存时,SizeOfImage指定了PEImage在虚拟内存中所占空间的大小。一般而言,文件的大小与加载到内存中的大小是不同的(节区头中定义了各节装载的位置与占有内存的大小)

6 .SizeOfHeader
SizeOfHeader用来指出整个PE头的大小。该值也必须是FileAlignment的整数倍。第一节区所在位置与SizeOfHeader距文件开始偏移的量相同。

7 .Subsystem
该Subsystem值用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe,*.dll)。Subsystem成员可拥有的值如表所示。
Pasted image 20250313141539

8 .NumberOfRvaAndSizes
NumberOfRvaAndSizes用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)数组的个数。虽然结构体定义中明确指出了数组个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16),但是PE装载器通过查看NumberOfRvaAndSizes值来识别数组大小,换言之,数组大小也可能不是16。

9 .DataDirectory
DataDirectory是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值。 其中最重要的是 EXPORT/IMPORT/RESOURCE、TLSDirection。特别需要注意的是IMPORT与EXPORT Directory(导出表与导入表)
Pasted image 20250313142002

3.4. 节区头

节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。
Pasted image 20250313143711
节区头中定义了各节区属性。看节区头之前先思考一下:前面提到过,PE文件中的code(代码)、data(数据入、resource(资源)等按照属性分类存储在不同节区,设计PE文件格式的工程师们之所以这样做,一定有着某些好处。

如可以保证程序的安全性。若把code与data放在一个节区中相互纠缠(实际上完全可以这样做)很容易引发安全问题,即使忽略过程的烦琐。
假如向字符串data写数据时,由于某个原因导致溢出(输人超过缓冲区大小时),那么其下的code(指令)就会被覆盖,应用程序就会崩溃。因此,PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为“节区”的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等)。
Pasted image 20250313142351

3.4.1. VirtualAddress与PointerToRawData

Pasted image 20250313143643
VirtualAddress与PointerToRawData不带有任何值,分别由(定义在IMAGE_OPTIONAL_HEADER32中的)SectionAlignment与FileAlignment确定。

VirtualSize与SizeOfRawData一般具有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的。

3.4.2. Name字段

Pasted image 20250313143652
Name成员不像C语言中的字符串一样以NULL结束,并且没有“必须使用ASCI值”的限制。PE规范未明确规定节区的Name,所以可以向其中放人任何值,甚至可以填充NULL值。所以节区的Name仅供参考,不能保证其百分之百地被用作某种信息(数据节区的名称也可叫做.code)。

4. RVA to RAW

讲解PE文件时经常出现“映像”(Image)这一术语,希望各位牢记。PE文件加载到内存时,文件不会原封不动地加载,而要根据节区头中定义的节区起始地址、节区大小等加载。因此,磁盘文件中的PE与内存中的PE具有不同形态。将装载到内存中的形态称为“映像”以示区别,使用这一术语能够很好地区分二者。

理解了节区头后,下面继续讲解有关PE文件从磁盘到内存映射的内容。
PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移间的映射。这种映射一般称为RVA to RAW
方法如下。
(1)查找RVA所在节区。
(2)使用简单的公式计算文件偏移(RAW)。
根据IMAGE_SECTION_HEADER结构体,换算公式如下:

4.1. 练习题-计算notepad.exe的文件与内存间的映射关系

Pasted image 20250316135823

VA:进程虚拟内存的绝对地址
RVA:相对虚拟地址(从某个基准位置(ImageBase)开始的相对地址)
ImageBase:基准位置
RVA+ImageBase=VA
从图中可知
Pasted image 20250316143605

4.1.1. Q1: RVA相对虚拟地址=5000时,文件偏移 RAW 是多少

首先查看RVA所在的节区
RVA 5000处于第一个节区.text(假设imageBase为 01000000
使用公式计算

4.1.2. Q2: RVA=13314时,FIle Offset=?

13314处于第三个节区 .rsrc
公式计算

4.1.3. RVA=ABA8时,FIle Offset=?

ABA8处于第二给节区 .data

计算结果为 RAW=97A8,但是该偏移在第三个节区(.rsrc)。RVA在第二个节区,而RAW在第三个节区,这显然是错误的。该情况表明“无法定义与RVA(ABA8)相对应的RAW值”。出现以上情况的原因在于,第二个节区的 VirtualSize 值要比 SizeOfRawData 值大。
即:RVA(Relative Virtual Address,相对虚拟地址)和 RAW(文件偏移)必须在同一个节(Section)内,才能正确计算出 RAW 值

5. IAT 导入地址表

IAT(Import Address Table,导人地址表)。IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构等有关。
换句话说,只要理解了IAT,就掌握了Windows操作系统的根基。简言之,IAT是一种表格,用来记录程序正在使用哪些库中的哪些函数

5.1. DLL 动态链接库

16位的DOS时代不存在DLL这一概念,只有“库”(Library)一说。比如在C语言中使用 printf() 函数时,编译器会先从C库中读取相应函数的二进制代码,然后插人(包含到)应用程序。也就是说,可执行文件中包含着 printf 函数的二进制代码。WindowsOS支持多任务,若仍采用这种包含库的方式,会非常没有效率。Windows操作系统使用了数量庞大的库函数(进程、内存、窗口、消息等)来支持32位的Windows环境。同时运行多个程序时,若仍像以前一样每个程序运行时都包含相同的库,将造成严重的内存浪费(当然磁盘空间的浪费也不容小觑)。因此,WindowsOS设计者们根据需要引入了DLL这一概念,DLL描述如下

  • 不要把库包含到程序中,单独组成DLL文件,需要时调用即可。
  • 内存映射技术使加载后的DLL代码、资源在多个进程中实现共享。
  • 更新库时只要替换相关DLL文件即可,简便易行。

加载DLL的两种方式

  • 显式链接:程序使用DLL的时候加载,使用完后就释放内存
  • 隐式链接:程序开始时即一同加载DLL,程序终止时再释放占用的内存。(IAT提供的机制与此有关)

使用OD打开 notepad.exe,然后查看IAT,比如我们要查看程序如何调用位于 kernel32.dllCreateFileW() 函数
所有模块间的引用中找到对应的函数
Pasted image 20250316161651
调用 CreateFileW() 函数时并非是直接调用,而是通过获取 01001104 地址处的值来实现的(所有API都采用这种方式)
Pasted image 20250316161911
地址 01001104notepad.exe.text 节区的内存区域(更确切说是IAT内存区域)。此处的值是 756833E0,而地址 756833E0 即是加载到进程内存中的 CreateFileW() 函数(位于 Kernel32.dll 库中)的地址。

Question

这里为什么不直接使用call 756833E0指令调用函数,这样不是更方便,更好吗?

事实上,notepad.exe程序的制作者编译(生成)程序时,并不知道notepad.exe程序要运行在哪种Windows(9X、2K、XP、Vista、7)、哪种语言(ENG、JPN、KOR等)、哪种服务包(ServicePack)下。上面列举出的所有环境中,kernel32.dll的版本各不相同,CreateFileWO函数的位置(地址)也不相同。
为了确保在所有环境中都能正常调用CreateFileW函数,编译器准备了要保存CreateFileW函数实际地址的位置(01001104),并仅记下CALL DWORD PTR DS[1004404]形式的指令。执行文件时,PE装载器将CreateFileWO函数的地址写到01001104位置。

另一个原因:DLL重定位 DLL文件的ImageBase值一般为10000000。比如某个程序使用a.dll与b.dll时,P装载器先把a.dll装载到内存的10000000ImageBase)处,然后尝试把b.dll也装载到该处。但是由于该地址处已经装载了a.dll,所以PE装载器查找其他空白的内存空间(ex:3E000000),然后将b.dll装载进去。
这就是所谓的DLL重定位,它使我们无法对实际地址硬编码。
另一个原因在于,PE头中表示地址时不使用VA,而是RVA

实际操作中无法保证DLL一定会被加载到PE头内指定的ImageBase处。但是EXE文件(生成进程的主体)却能准确加载到自身的ImageBase中,因为它拥有自己的虚拟空间。

5.2. IMAGE_IMPORT_DESCRIPTOR

IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导人哪些库文件。

Import:导入,向库提供服务(函数)。
Export:导出,从库向其他PE文件提供服务(函数)。

执行一个普通程序时往往需要导人多个库,导人多少库就存在多少个 IMAGE_IMPORT_DESCRIPTOR 结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体结束。IMAGE_IMPORT_DESCRIPTOR 中的重要成员如表所示(拥有全部RVA值)。

项目 含义
OriginalFirstThunk INT的地址(RVA)
Name 库名称字符串的地址(RVA)
FirstThunk IAT的地址(RVA)
  • PE头中提到的“Table”即指数组。
  • INT(导入名称表) IAT(导入地址表)
  • INT与IAT是长整型(4个字节数据类型)数组,以NULL结束(未另外明确指出大小)。
  • INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针(有时IAT也拥有相同的值)。
  • INT与IAT的大小应相同。

5.3. 使用notepad.exe练习

5.3.1. IMAGE_IMPORT_DESCRIPTOR结构体数组

首先明白 IMAGE_IMPORT_DESCRIPTOR 结构体数组存在于PE文件的PE体中,但是查找起位置的信息在PE头中。但查找其位置的信息在PE头中
IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 的值即是 IMAGE_IMPORT_DESCRIPTOR 结构体数组的起始地址(RVA值)。
IMAGE_IMPORT_DESCRIPTOR 结构体数组也被称为 IMPORT Directory Table(只有了解上述全部称谓,与他人交流时才能没有障碍)

IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 结构体的值如图,(第一个4字节为虚拟地址,第二个4字节为Size成员)
Pasted image 20250316164600

整理一下 IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 结构体数组的信息

偏移 说明
00000158 00000000 RVA of EXPORT Directory
0000015C 00000000 size of EXPORT Directory
00000160 00007604 RVA of IMPORT Directory
00000164 000000C8 size of IMPORT Dirctory
00000168 0000BOO0 RVA of RESOURCE Directory
00000016C 00008304 size of RESOURCE Directory

Pasted image 20250316165054
这里我们可以计算一下 RVA=7604 的文件偏移 RAW
图中可以知道,.text 节区的VA是 0400h SIZE是 7800H
Pasted image 20250316165937

根据公式进行计算

Warning

这里可能有问题。书中给的答案是6A04h,但是我用010Editor看的第一个节区的VA是400h 有问题。
这里可以直接用010看对应的RAW
Pasted image 20250316172610

转到6a04处,这些都是 IMAGE_IMPORT_DESCRIPTOR 结构体数组,图中就是结构体数组的第一个元素
Pasted image 20250316172714

5.3.2. notepad.exe文件的第一个IMAGE_IMPORT_DESCRIPTOR结构体

IMAGE_IMPORT_DESCRIPTOR 结构体数组的第一个结构为例,看一下其结构体的成员
Pasted image 20250316183141

文件偏移 成员 RVA RAW
6A04 OriginalFirstThunk(INT) 00007990 00006D90
6A08 TimeDateStamp FFFFFFFF -
6A0C ForwarderChain FFFFFFFF -
6A10 Name 00007AAC 00006EAC
6A14 FirstThunk(IAT) 000012C4 000006C4
5.3.2.1. 库名称 Name
  • Name是一个字符串指针,它指向导人函数所属的库文件名称。
    Pasted image 20250316183706
5.3.2.2. OriginalFirstThunk(INT)
  • INT是一个包含导人函数信息(Ordinal,Name)的结构体指针数组。只有获得了这些信息,才能在加载到进程内存的库中准确求得相应函数的起始地址
    我们跟踪 OriginalFirstThunk 成员(RVA:7990->RAW:6D90)

    图中就是INT (导入名字表)
    Pasted image 20250316190318
    可以看到,INT由地址数组形式组成 数组尾部以 null 结束 ,每个地址值分别指向 IMAGE_IMPORT_BY_NAME 结构体。 跟踪数组第一个值 7A7A ,进入对应的地址。可以看到导入的API函数的名称字符串
    Pasted image 20250316190747
5.3.2.3. IMAGE_IMPORT_BY_NAME

RVA:7A7A 即为 RAW: 6E7A
文件偏移 6E7A 最初的2个字节值(000F)为Ordinal,是库中函数的固有编号。Ordinal的后面为函数名称字符串 PageSetupDlgW(同C语言一样,字符串末尾以 TerminatingNULL['\O'] 结束)。

如图,INT是 IMAGE_IMPORT_BY_NAME 结构体指针数组,数组的第一个元素指向函数的 Ordinal000F 函数的名称为 PageSetupDlgW
Pasted image 20250316191320

5.3.2.4. FirstThunk-IAT (Import Address Table)

IAT的RVA: 12C4 即为RAW:6C4
图中文件偏移 6C4~6EB 区域即为IAT数组区域,对应于 comdlg32.dIl 库。它与INT类似,由结构体指针数组组成,且以NULL结尾。IAT的第一个元素值被硬编码为76324906,该值无实际意义,notepad.exe文件加载到内存时准确的地址值会取代该值。
Pasted image 20250316191736

作者的话
  • 其实我的系统(WindowsXPSP3)中,地址76324906即是comdlg32.dll!PageSetupDlgW函数的准确地址值。但是该文件在Windows7中也能顺利运行。运行notepad.exe进程时,PE装载器会使用相应API的起始地址替换该值。
  • 微软在制作服务包过程中重建相关系统文件,此时会硬编入准确地址(普通的DLL实际地址不会被硬编码到IAT中,通常带有与INT相同的值)。
  • 另外,普通DLL文件的ImageBase为10000000,所以经常会发生DLL重定位。但是Windows系统DLL文件(kernel32/user32/gdi32等)拥有自身固有的ImageBase,不会出现DLL重定位。
5.3.2.5. OD查看notepad.exe的IAT

Pasted image 20250316192107 notepad.exeImageBase 的值为 01000000comdlg32.dll.PageSetupDlgW 函数的IAT地址为 010012C4,其值为 75B46730,它是API准确的起始地址值。

进入 75B46730 地址中。可以看到这里就是 comdlg32.dllPageSetupDlgW 函数的起始位置。
Pasted image 20250316192426

6. EAT

Windows操作系统中,“库”是为了方便其他程序调用而集中包含相关函数的文件(DLL/SYS)。Win32API是最具代表性的库,其中的 kernel32.dll 文件被称为最核心的库文件。
EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数。也就是说,只有通过EAT才能准确求得从相应库中导出函数的起始地址。与前面讲解的IAT一样,PE文件内的特定结构体(IMAGEEXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY结构体。

用来说明IAT的 IMAGE_IMPORT_DESCRIPTOR 结构体以数组形式存在,且拥有多个成员。这样是因为PE文件可以同时导入多个库。

可以在PE文件的PE头中查找到 IMAGE_EXPORT_DIRECTORY 结构体的位置。IMAGE_OPTIONAL_HEADER32.DataDirectory[O].VirtualAddress 值即是 IMAGE_EXPORT_DIRECTORY 结构体数组的起始地址(也是RVA的值)。

图中就是 kernel32.dll 文件的 IMAGE_OPTIONAL_HEADER32.DataDirectory[0](第一个4字节为VirtualAddress,第二个4字节为Size成员
Pasted image 20250316193243

下表为 kernel32.dll 文件的 DataDirectory 数组 -Export

偏移 说明
00000160 00000000 loader flags
00000164 00000010 number of directories
00000168 0000262C RVA of EXPORT Directory
0000016C 00006D19 size of EXPORT Directory
00000170 00081898 RVA of IMPORT Directory
00000174 00000028 size of IMPORT Directory

Pasted image 20250316194111
RVA为262C 则 文件偏移就是 1A2C

6.1. IMAGE_EXPORT_DIRECTORY 结构体

IMAGE_EXPORT_DIRECTORY 是一个 结构体。它是 PE(Portable Executable)文件格式中用于描述导出表(Export Table)的数据结构。每个 PE 文件(如 DLL 或 EXE)的导出表通常只有一个 IMAGE_EXPORT_DIRECTORY 结构体实例。

下面是其中的一些重要成员

项目 含义
NumberOfFunctions 实际 Export 函数的个数
NumberOfNames Export 函数中具名的函数个数
AddressOfFunctions Export 函数地址数组(数组元素个数=NumberOfFunctions)
AddressOfNames 函数名称地址数组(数组元素个数=NumberOfNames)
AddressOfNameOrdinals Ordinal 地址数组(数组元素个数=NumberOfNames)

从库中获得函数地址的API为 GetProcAddress() 函数。该API引用EAT来获取指定API的地址。GetProcAddress() API拥有函数名称,下面讲解它如何获取函数地址。理解了这一过程,就等于征服了EAT。

GetProcAddress()操作原理

(1)利用AddressOfNames成员转到“函数名称数组”。
(2)“函数名称数组”中存储着字符串地址。通过比较(strcmp)字符串查找指定的函数名称(此时数组的索引称为name_index)。
(3)利用AddressOfNameOrdinals成员,转到orinal数组。
(4)在ordinal数组中通过name_index查找相应ordinal值。
(5)利用AddressOfFunctions成员转到“函数地址数组”(EAT)。
(6)在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定数的起始地址。

Pasted image 20250316220743
此图描述的是kernel32.dll文件的情形。kernel32.dll中所有导出函数均有相应名称,AddressOfNameOrdinals数组的值以index=ordinal的形式存在。但并不是所有的DLL文件都如此。导出函数中也有一些函数没有名称(仅通过ordinal导出),AddressOfNameOrdinals数组的值为index!=ordinal。所以只有按照上面的顺序才能获得准确的函数地址。

6.2. 使用 kernel32.dll 练习

下面进行从 kernel32.dll 文件的EAT中查找 AddAtomW 函数。上面我们计算出 kernel32.dllIMAGE_EXPORT_DIRECTORY 结构体的RAW为 1A2C。利用010查看,我们直接跳到对应的位置
Pasted image 20250316222342
上图就是 IMAGE_EXPORT_DIRECTORY 结构体对应的区域,如下是此结构体各个成员的属性

文件偏移 成员 RAW
1A2C Characteristics 00000000 -
1A30 TimeDateStamp 48025BE1 -
1A34 MajorVersion 0000 -
1A36 MinorVersion 0000 -
1A38 Name 00004B8E 3F8E
1A3C Base 00000001 -
1A40 NumberOfFuctions 000003B9 -
1A44 NumberOfNames 000003B9 -
1A48 AddressOfFunctions 00002654 1A54
1A4C AddressOfNames 00003538 2938
1A50 AddressOfNameOrdinals 0000441C 381C

6.2.1. 函数名称的数组

看表得到 AddressOfNames 的成员值为 RVA=3538 计算出 RAW=2938。使用010查看对应的地址
Pasted image 20250316225042
此处为4字节RVA组成的数组。数组的元素个数为 NumberOfNames = 3B9 个。逐一跟随所有的RVA值即可发现函数名称字符串。

6.2.2. 查找指定函数名称

要查找的函数名称字符串为“AddAtomW”,只要在上图中找到RVA数组第三个元素的值 (RVA:4BB3-RAW:3FB3) 即可。
Pasted image 20250316225136
进人相应地址就会看到“AddAtomW”字符串,如图所示。此时“AddAtomW”函数名即是上图数组的第三个元素,数组索引为2。
Pasted image 20250316225213