PE文件是Windows操作系统下使用的可执行文件格式。它是微软在UNIX平台的COFF(Common Object File Format),通用对象文件格式)基础上制作而成的。最初(正如Portable这个单词所代表的那样)设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列的操作系统下。
PE文件是指32位的可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)文件的一种扩展形式(请注意不是PE64)。
种类 | 主拓展名 |
---|---|
可执行系列 | 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头中的结构体。
从DOS头(DOSheader)到节区头(Section header)是PE头部分,其下的节区合称PE体。文件中使用偏移(offset),内存中使用VA(VirtualAddress,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区的大小、位置等)。
文件的内容一般可分为代码(.text)、数据(.data)、资源(.rsrc)节,分别保存。
根据所用的不同开发工具(VB/VC++/Delphi/etc)与编译选项,节区的名称、大小、个数、存储的内容等都是不同的。最重要的是它们按照不同的用途分类保存到不同的节中。
各节区头定义了各节区在文件或内存中的大小、位置、属性等。
PE头与各节区的尾部存在一个区域,称为NULL填充(NULLpadding)。计算机中,为了提高处理文件、内存、网络包的效率,使用“最小基本单位”这一概念,
PE文件中也类似。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数位置上,空白区域将用NULL填充(看图,可以看到各节区起始地址的截断都遵循一定规则)。
PE头内部信息大多以RVA形式存在。原因在于,PE文件(主要是DLL)加载到进程虚拟内
存的特定位置时,该位置可能已经加载了其他PE文件(DLL)。此时必须通过重定位(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息,不会出现任何问题。
32位WindowsOS中,各进程分配有4GB的虚拟内存,因此进程中VA值的范围是
00000000~FFFFFFFF
。
64位是0x0000000000000000
~0xFFFFFFFFFFFFFFFF
(16EB)
PE头由许多结构体组成
微软创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑了PE文件对DOS文件的兼容性。其结果是在PE头的最前面添加了一个 IMAGE_DOS_HEADER
结构体,用来扩展已有的DOS EXE头。
如图
IMAGE_DOS_HEADER
结构体的大小为 0x40个字节。 主要由两个重要成员
这里的NT头偏移是
000000E0
,因为Intel系列的CPU以逆序存储数据,这称为小端序标识法
如果我们修改这两处任意一个值,都会发现程序无法运行(因为根据PE规范,它已经不是PE文件了)
DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(即使没有DOS存根,文件也能正常运行)。DOS存根由代码与数据混合而成
其中40-4D处的16进制是一串16位的汇编指令,32位的windowsOS中不会运行此命令(由于被识别为PE文件,所以完全忽视该代码)
如果在DOS环境中运行记事本,或者使用调试器运行,就会输出 This program cannot be run in DOS mode
然后就退出
NT头 IMAGE_NT_HEADERS
结构体由3个成员组成
32位NT头结构体大小为 0xF8 248字节 (4 + 20 + 224)
64位NT头结构体大小为0x108 264字节(4 + 20 + 240)
文件头 IMAGE_FILE_HEADERS
结构体中有如下4种重要成员(若它们设置不正确,将导致文件无法正常运行)。
1.Machine
每个CPU都拥有唯一的Machine码,兼容32位Intelx86芯片的Machine码为14C。
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_HEADER
的 SizeOfOptionalHeader
值,从而识别出 IMAGE_OPTIONAL_HEADER32
结构体的大小。
PE32+格式的文件中使用的是 IMAGE_OPTIONAL_HEADER64
结构体,而不是 IMAGEOPTIONAL_HEADER32
结构体。2个结构体的尺寸是不同的,所以需要 SizeOfOptionalHeader
成员中明确指出结构体的大小。
借助
IMAGE_DOS_HEADER
的e_lfanew
成员与IMAGE_FILE_HEADER
的SizeOfOptionalHeader
成员,可以创建出一种脱离常规的PE文件(PEPatch)(也有人称之为“麻花”PE文件)。
4.CHaracteristics
该字段用于标识文件的属性,文件是否是可运行的形态、是否为DLL文件等信息,以bitOR形式组合起来。
以下是定义在 winnt.h
文件中的Characteristics值(请记住0002h与2000h这两个值)
另外,PE文件中Characteristics的值可以不是0002h(不可执行的),比如类似*.obj的object文件及resourceDLL文件等。
5. 不影响文件运行的 TimeDateStampA
成员
该成员的值不影响文件运行,用来记录编译器创建此文件的时间。但是有些开发工具(VB、VC++)提供了设置该值的工具,而有些开发工具(Delphi)则未提供(且随所用选项的不同而不同)
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成员可拥有的值如表所示。
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(导出表与导入表)
节区头是由IMAGE_SECTION_HEADER
结构体组成的数组,每个结构体对应一个节区。
节区头中定义了各节区属性。看节区头之前先思考一下:前面提到过,PE文件中的code(代码)、data(数据入、resource(资源)等按照属性分类存储在不同节区,设计PE文件格式的工程师们之所以这样做,一定有着某些好处。
如可以保证程序的安全性。若把code与data放在一个节区中相互纠缠(实际上完全可以这样做)很容易引发安全问题,即使忽略过程的烦琐。
假如向字符串data写数据时,由于某个原因导致溢出(输人超过缓冲区大小时),那么其下的code(指令)就会被覆盖,应用程序就会崩溃。因此,PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为“节区”的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等)。
VirtualAddress与PointerToRawData不带有任何值,分别由(定义在IMAGE_OPTIONAL_HEADER32
中的)SectionAlignment与FileAlignment确定。
VirtualSize与SizeOfRawData一般具有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的。
Name成员不像C语言中的字符串一样以NULL结束,并且没有“必须使用ASCI值”的限制。PE规范未明确规定节区的Name,所以可以向其中放人任何值,甚至可以填充NULL值。所以节区的Name仅供参考,不能保证其百分之百地被用作某种信息(数据节区的名称也可叫做.code)。
讲解PE文件时经常出现“映像”(Image)这一术语,希望各位牢记。PE文件加载到内存时,文件不会原封不动地加载,而要根据节区头中定义的节区起始地址、节区大小等加载。因此,磁盘文件中的PE与内存中的PE具有不同形态。将装载到内存中的形态称为“映像”以示区别,使用这一术语能够很好地区分二者。
理解了节区头后,下面继续讲解有关PE文件从磁盘到内存映射的内容。
PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移间的映射。这种映射一般称为RVA to RAW
方法如下。
(1)查找RVA所在节区。
(2)使用简单的公式计算文件偏移(RAW)。
根据IMAGE_SECTION_HEADER结构体,换算公式如下:
VA:进程虚拟内存的绝对地址
RVA:相对虚拟地址(从某个基准位置(ImageBase)开始的相对地址)
ImageBase:基准位置
RVA+ImageBase=VA
从图中可知
相对虚拟地址
=5000时,文件偏移 RAW
是多少首先查看RVA所在的节区
RVA 5000
处于第一个节区.text
(假设imageBase为 01000000
)
使用公式计算
13314
处于第三个节区 .rsrc
公式计算
ABA8
处于第二给节区 .data
计算结果为
RAW=97A8
,但是该偏移在第三个节区(.rsrc)。RVA在第二个节区,而RAW在第三个节区,这显然是错误的。该情况表明“无法定义与RVA(ABA8)相对应的RAW值”。出现以上情况的原因在于,第二个节区的VirtualSize
值要比SizeOfRawData
值大。
即:RVA(Relative Virtual Address,相对虚拟地址)和 RAW(文件偏移)必须在同一个节(Section)内,才能正确计算出 RAW 值
IAT(Import Address Table,导人地址表)。IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构等有关。
换句话说,只要理解了IAT,就掌握了Windows操作系统的根基。简言之,IAT是一种表格,用来记录程序正在使用哪些库中的哪些函数。
16位的DOS时代不存在DLL这一概念,只有“库”(Library)一说。比如在C语言中使用 printf()
函数时,编译器会先从C库中读取相应函数的二进制代码,然后插人(包含到)应用程序。也就是说,可执行文件中包含着 printf
函数的二进制代码。WindowsOS支持多任务,若仍采用这种包含库的方式,会非常没有效率。Windows操作系统使用了数量庞大的库函数(进程、内存、窗口、消息等)来支持32位的Windows环境。同时运行多个程序时,若仍像以前一样每个程序运行时都包含相同的库,将造成严重的内存浪费(当然磁盘空间的浪费也不容小觑)。因此,WindowsOS设计者们根据需要引入了DLL这一概念,DLL描述如下。
加载DLL的两种方式
使用OD打开 notepad.exe
,然后查看IAT,比如我们要查看程序如何调用位于 kernel32.dll
中 CreateFileW()
函数
在 所有模块间的引用中找到对应的函数
调用 CreateFileW()
函数时并非是直接调用,而是通过获取 01001104
地址处的值来实现的(所有API都采用这种方式)
地址 01001104
是 notepad.exe
中 .text
节区的内存区域(更确切说是IAT内存区域)。此处的值是 756833E0
,而地址 756833E0
即是加载到进程内存中的 CreateFileW()
函数(位于 Kernel32.dll
库中)的地址。
这里为什么不直接使用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中,因为它拥有自己的虚拟空间。
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的大小应相同。
首先明白 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成员)
整理一下 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 |
这里我们可以计算一下 RVA=7604
的文件偏移 RAW
图中可以知道,.text
节区的VA是 0400h
SIZE是 7800H
根据公式进行计算
这里可能有问题。书中给的答案是6A04h,但是我用010Editor看的第一个节区的VA是400h 有问题。
这里可以直接用010看对应的RAW
转到6a04
处,这些都是 IMAGE_IMPORT_DESCRIPTOR
结构体数组,图中就是结构体数组的第一个元素
IMAGE_IMPORT_DESCRIPTOR
结构体以 IMAGE_IMPORT_DESCRIPTOR
结构体数组的第一个结构为例,看一下其结构体的成员
文件偏移 | 成员 | RVA | RAW |
---|---|---|---|
6A04 | OriginalFirstThunk(INT) | 00007990 | 00006D90 |
6A08 | TimeDateStamp | FFFFFFFF | - |
6A0C | ForwarderChain | FFFFFFFF | - |
6A10 | Name | 00007AAC | 00006EAC |
6A14 | FirstThunk(IAT) | 000012C4 | 000006C4 |
OriginalFirstThunk
成员(RVA:7990->RAW:6D90)null
结束 ,每个地址值分别指向 IMAGE_IMPORT_BY_NAME
结构体。 跟踪数组第一个值 7A7A
,进入对应的地址。可以看到导入的API函数的名称字符串RVA:7A7A
即为 RAW: 6E7A
文件偏移 6E7A
最初的2个字节值(000F)为Ordinal,是库中函数的固有编号。Ordinal的后面为函数名称字符串 PageSetupDlgW
(同C语言一样,字符串末尾以 TerminatingNULL['\O']
结束)。
如图,INT是 IMAGE_IMPORT_BY_NAME
结构体指针数组,数组的第一个元素指向函数的 Ordinal
值 000F
函数的名称为 PageSetupDlgW
IAT的RVA: 12C4
即为RAW:6C4
图中文件偏移 6C4~6EB
区域即为IAT数组区域,对应于 comdlg32.dIl
库。它与INT类似,由结构体指针数组组成,且以NULL结尾。IAT的第一个元素值被硬编码为76324906,该值无实际意义,notepad.exe文件加载到内存时准确的地址值会取代该值。
notepad.exe
的 ImageBase
的值为 01000000
。comdlg32.dll.PageSetupDlgW
函数的IAT地址为 010012C4
,其值为 75B46730
,它是API准确的起始地址值。
进入 75B46730
地址中。可以看到这里就是 comdlg32.dll
的 PageSetupDlgW
函数的起始位置。
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成员
下表为 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 |
RVA为262C
则 文件偏移就是 1A2C
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。
(1)利用AddressOfNames
成员转到“函数名称数组”。
(2)“函数名称数组”中存储着字符串地址。通过比较(strcmp)字符串查找指定的函数名称(此时数组的索引称为name_index
)。
(3)利用AddressOfNameOrdinals
成员,转到orinal数组。
(4)在ordinal
数组中通过name_index查找相应ordinal值。
(5)利用AddressOfFunctions
成员转到“函数地址数组”(EAT)。
(6)在“函数地址数组”中将刚刚求得的ordinal
用作数组索引,获得指定数的起始地址。
此图描述的是kernel32.dll
文件的情形。kernel32.dll
中所有导出函数均有相应名称,AddressOfNameOrdinals
数组的值以index=ordinal
的形式存在。但并不是所有的DLL文件都如此。导出函数中也有一些函数没有名称(仅通过ordinal导出),AddressOfNameOrdinals
数组的值为index!=ordinal
。所以只有按照上面的顺序才能获得准确的函数地址。
下面进行从 kernel32.dll
文件的EAT中查找 AddAtomW
函数。上面我们计算出 kernel32.dll
中 IMAGE_EXPORT_DIRECTORY
结构体的RAW为 1A2C
。利用010查看,我们直接跳到对应的位置
上图就是 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 |
看表得到 AddressOfNames
的成员值为 RVA=3538
计算出 RAW=2938
。使用010查看对应的地址
此处为4字节RVA组成的数组。数组的元素个数为 NumberOfNames = 3B9
个。逐一跟随所有的RVA值即可发现函数名称字符串。
要查找的函数名称字符串为“AddAtomW”,只要在上图中找到RVA数组第三个元素的值 (RVA:4BB3-RAW:3FB3)
即可。
进人相应地址就会看到“AddAtomW”字符串,如图所示。此时“AddAtomW”函数名即是上图数组的第三个元素,数组索引为2。