先简单了解一下,后期跟着"PE权威指南"详细学习
PE
(Portable文件是Windows
下使用的可执行文件格式
基于UNIX平台的COFF
(Common Object File Format 通用对象文件格式)基础上制作而成
基础概念
文件格式
种类 | 主拓展名 | 种类 | 主拓展名 |
---|---|---|---|
可执行 | EXE,SCR | 驱动程序 | SYS,VXD |
库系列 | DLL,OCX,CPL,DRV | 对象文件 | OBJ |
严格来说,除了OBJ
的所有文件都可以执行,只是DLL
,SYS
等文件不能直接在SHELL
(explorer.exe)中执行,但是可以在其他环境下(调试器,服务)等执行
PE装载
- 当PE文件被执行,PE装载器检查
DOS MZ header
里的PE header
偏移量,如果找到,则跳转到PE header
PE
装载器检查PE header
的有效性,如果有效,则跳转到PE header
的尾部PE header
后面是节表。PE装载器读取其中的节信息,并采用文件映射的方法,将这些节映射到内存,同时赋上节表里指定的节属性- PE文件映射入内存后,
PE
装载器将处理PE
文件中类似import table
引入表逻辑部分
PE组成
PE文件大体分为两部分:
头 (包括下图中的DOS头,PE文件头,块表 Section Table)
主体 ( 块Section)。
PE文件头
PE头 IMAGE_NT_HEADERS
包含三部分
- “PE\0\0”
- IMAGE_FILE_HEADER
- IMAGE_OPTIONAL_HEADER32
扩展PE头/可选头(IMAGE_OPTIONAL_HEADER32)
IMAGE_DATA_DIRECTORY
数据目录项,组成有
- 导出表、
- 导入表、
- 资源表、
- 异常处理表、
- 安全表、(Certificate Table)
- 重定位表、
- 调试表、
- 版权、
- 指针目录、
- TLS、
- 载入配置、
- 绑定输入目录、
- 导入地址表、
- 延迟载入、
- COM信息。
地址转换
虚拟内存
文件偏移地址(File Offset Address) :文件相对于文件开头的偏移
装载基址(Image Base) :PE装入内存时的基地址,EXE在内存中的基地址是0x00400000,DLL的基地址是0x10000000,这些位置可以通过修改编译选项更改
虚拟内存地址 (Virtual Address, VA) :PE文件中的指令被装入内存后的地址,在Windows系统中,PE文件被系统加载器映射到内存中。每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址(Virtual Address,VA)。
相对虚拟地址(Relative Virtual Address,RVA) :相对虚拟地址是内存地址相对于映射基地址偏移量
VA=ImageBase+RVA
节偏移
节偏移 :PE文件中的数据按照磁盘数据标准存放,以0x200字节为基本单位进行组织,当PE文件装载到内存时,将按照内存数据标准存放,以0x1000字节为基本单位,所以文件偏移地址和相对虚拟内存会有细微的差别,这种差别称为节偏移
SectionAlignment : 内存当中的块对齐数,一般为0x1000。
FileAlignment :磁盘当中块对齐数,一般为0x200。
文件偏移地址 = 虚拟内存地址(VA)- 装载基址(ImageBase)- 节偏移
= RVA –节偏移
入口点(OEP)
入口点(Original Entry Point) : 首先明确一个概念就是OEP是一个RVA,然后使用OEP +Imagebase ==入口点的VA,通常情况下,OEP指向的不是main函数。
(往往是CRT)
内存映像
PE文件不是作为单一内存映射文件,被载入的内存是很重要的。Windows加载器(又称PE装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被载入内存,磁盘上的数据结构布局和内存中的数据结构布局就是一致的。
当PE文件通过Windows
加载器载入内存后,PE在内存中的版本称为模块(Module)
映射文件的起始地址称为模块句柄(hModule)
可以通过模块句柄访问内存中的其他数据结构,这个初始内存地址也称为基地址ImageBase
可执行程序在内存中的组成
一般情况下,一个可执行C程序在内存中主要包含5个区域,分别是**代码段(text),数据段(data),BSS段,堆段(heap)和栈段(stack)。**其中前三个段(text,data,bss)是程序编译完成就存在的,此时程序并未载入内存进行执行。后两个段(heap,stack)是程序被加载到内存中时,才存在的。具体的样子可以如下图所示:
未初始化的全局变量和静态变量,存储在bss区, 未初始化的局部变量保存在栈区。
代码段(text):就是C程序编译后的机器指令,也就是我们常见的汇编代码。
数据段(data):用来存放显式初始化的全局变量或者静态(全局)变量,常量数据。
BSS段(Block Started by Symbol): 存储未初始化的全局变量或者静态(全局)变量。编译器给处理成0;
栈段(stack):存放函数调用相关的参数、局部变量的值,以及在任务切换的上下文信息。栈区是由操作系统分配和管理的区域。
堆段(heap): 动态内存分配的区域,也就是malloc申请的内存区,使用free()函数来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏。
包含data段和bss段的整个区段此时通常称为数据区。
auto存储类型:auto只能用来标识局部变量的存储类型,对于局部变量,auto是默认的存储类型,不需要显示的指定。因此,auto标识的变量存储在栈区中。
extern存储类型:extern用来声明在当前文件中引用在当前项目中的其它文件中定义的全局变量。如果全局变量未被初始化,那么将被存在BBS区中,且在编译时,自动将其值赋值为0,如果已经被初始化,那么就被存在数据区中。
register存储类型:声明为register的变量在由内存调入到CPU寄存器后,则常驻在CPU的寄存器中,因此访问register变量将在很大程度上提高效率,因为省去了变量由内存调入到寄存器过程中的好几个指令周期。在C++中,例如 while(i–){}; 对变量 i 有频繁的操作,编译器会将其存储在寄存器中。
static存储类型:被声明为静态类型的变量,无论是全局的还是局部的,都存储在数据区中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{}内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。静态变量只能够初始化一次。
字符串常量:字符串常量存储在数据区中,其生存期为整个程序运行时间,但作用域为当前文件。
PE头
DOS头
这是微软考虑PE文件对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;
IMAGE_DOS_HEADER结构体的大小为40个字节
必须知道的两个重要成员
-
e_magic DOS签名 4D5A - > “MZ”
-
e_lfanew指示NT头的偏移(不同文件值不同)
DOSstub
DOS存根(stub)在IAMGE_DOS_HEADER下方
由代码与数据混合而成,即使没有DOS存根,文件也能正常运行
NT头
IMAGE_NT_HEADERS结构体的大小为F8,很大
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- Signature 值为0x50450000h 即PE00
- FILE Header 文件头
- OptionalHeader 可选头文件结构体
NT头:FILE Header
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;
有4个比较重要的成员
-
Machine 每个不同种类的CPU都有唯一的
Machine
码#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 // ARM Thumb/Thumb-2 Little-Endian #define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian #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_ARM64 0xAA64 // ARM64 Little-Endian #define IMAGE_FILE_MACHINE_CEE 0xC0EE
-
NumberOfSections
PE文件把代码,数据,资源等依据属性分类到各节中存储
这个值指出了文件中存在的节区数量,值比大于0,如果定义的节区数量与实际情况不符,运行错误
-
SizeOfOptionalHeader
IMAGE_NT_HEADER
结构体的最后一个成员为IAMGE_OPTIONAL_HEADER32
结构体而这个值用于指出
IMAGE_OPTIONAL_HEADER32
结构体的长度 -
Characteristics
该字段用于标识文件的属性,文件是否可运行的形态,是否为DLL等信息
以bit异或形式组合起来
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file. #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved external 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 // Aggressively 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文件头中的Characteristics可能不是0002h,比如*.obj
IMAGE_FILE_HEADER的TimeDataStamp成员,不影响文件运行,用来记录编译器创建该文件的时间
NT头:可选头
IMAGE_OPTIONAL_HEADER32
是PE头结构体中最大的
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
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;
文件运行必须包含指定的成员
-
Magic
为
IMAGE_OPTIONAL_HEADER32
结构体时,Magic
码为10B
;为
IMAGE_OPTIONALHEADER64
结构体时,Magic
码为20B
-
AddressOfEntryPoint
持有EP的RVA值,指出程序最先执行的代码起始地址
-
ImageBase
进程虚拟内存的范围是0~FFFFFFFF(32位)。PE文件被加载到如此大的内存中时,
ImageBase
指出文件的优先装入地址EXE,dll
等装载到用户内存的0x7FFFFFFF
SYS
文件被载入到内核内存的80000000-FFFFFFFF
中一般而言
EXE
的IMAGEBASE值为00400000DLL的IMAGEBASE值为10000000,也可以指定为其他值
执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为
IMAGEBASE+OEP
-
SectionAliginment,FileAlignment
PE文件的body部分划分为若干节区,这些节存储不同类型的数据
FileAlignment
指定了节区磁盘文件中的最小单位SectionAlignment
指定了节区在内存中的最小单位磁盘文件或内存的节区大小必定为
File/SecitonAlignment
的整数倍 -
SizeOfImage
加载PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占的空间大小(文件大小与加载的内存大小是不一样的,会经过映射
-
SizeOfHeader
用来指出整个PE头的大小,也必须是
FileAlignment
的整数倍 -
Subsystem
区别系统驱动文件(/sys)与普通的可执行文件(.dll,.exe)
值为1 Driver文件 系统驱动(ntfs.sys)
值为2 GUI文件 窗口应用程序(notepad.exe)
值为3 CUI文件 控制台应用程序(cmd.exe)
-
NumberOfRvaAndSizes
用来指定
DataDirectory
(即IMAGE_OPTIONAL_HEADER32
结构体的最后一个成员)数组的个数。定义上是16,也可能不是16 -
DataDirectory
这些数据段包括但不限于导入表、导出表、资源、异常处理信息、调试信息等。
IMAGE_DATA_DIRECTORY
结构体通常包含两个字段:VirtualAddress
:表示该数据段在进程虚拟地址空间Size
:表示该数据段的大小,以字节为单位。
节区头
PE
文件中的code
,data
,resource
等按照属性分类存储在不同节区
把PE文件创建成多个节区的结构的好处是 可以保证程序的安全性
若把code
与data
放在一个节区中相互纠缠很容易引发安全问题
加入我们向data写数据,由于某个原因导致溢出,那么code
就会被覆盖,程序就容易崩溃
因此PE
文件格式的设计者决定把具有相似属性的数据统一保存在一个被称为节区
的地方,并且把各节区属性记录在节区头中
类别 | 访问权限 |
---|---|
code | 执行,读取权限 |
data | 非执行,读写权限 |
resource | 非执行,读取权限 |
IMAGE_SECTION_HEADER
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
几个重要的成员
-
VirtualSize 内存中节区所占大小
-
VirtualAddress 内存中节区起始地址(RVA)
-
SizeOfRawData 磁盘文件中节区所占大小
-
PointerToRawData 磁盘文件中节区起始地址
-
Charateristics 节区属性(bit OR)
VirtualAddress
与PointerToRawData
不带有任何只。由定义在IMAGE_OPTIONAL_HEADER32
中的SectionAlignment
与FileAlignment
确定
VirtualSize
与SizeOfRawData
一般具有不同的值,因为
磁盘中和内存中的节区大小是不一样的
-
再了解一下
Name
字段,name成员不像C语言的字符串一样以NULL结束,可以放入任何值甚至填充NULL值 -
术语
IMAGE
实际上指的的内存中的PE映像
RVA to RAW
RAW是磁盘中数据对文件的偏移
PE文件从磁盘到内存映射的内容
节中数据的RAW-节头的RAW=节中数据RVA-节头的RVA
RAW-PointerToRawData=RVA-VirtualAddress
RAW=RVA-VirtualAddress+PointerToRawData
因此RAW=(RVA-节的VirtualAddress)+节的PonterToRawData
同理 RVA=RAW-节的PonterToRawData+节的VirtualAddress
几个练习
RVA=5000,File Offset=
RAW=5000(RVA)-1000(VirtualAddress)+400(PointerToRawData)=4400
FileOffset=RAW+ImageBse
IAT
IAT(Import Address Table 导入地址表)
IAT保存的内容与Windows操作系统的核心进程,内存,DLL结构等有关
DLL
DLL(DynamicLinkedLibrary)
- 不需要把库包含到程序,单独组成DLL文件,需要时调用即可
- 内存映射技术使加载后的DLL代码资源在多个进程中实现共享
- 更新库时只要替换相关DLL文件,操作简单
加载DLL的方式实际有两种
-
显式链接(Explicit Linking)
程序使用DLL时加载,使用完毕后释放内存
-
隐式链接(Implicit Linking)
程序开始时就加载DLL,程序终止时再释放占用的内存
例如调用CreateFileW
函数的代码(位于kernel32.dll)
调用CreateFileW()并非直接调用,而是通过获取01001104地址的值来实现
这个地址是*.exe中text节区的内存区域,这个区域的值为7C8107F0是加载到*.exe
进程内存中的CreateFileW()
函数(位于kernel32.dll)中的地址
为什么要这样操作,因为实际操作中无法保证DLL
一定会被加载到PE头内指定的ImageBase
处。但是EXE
文件却能准确加载到自身的ImageBase
IMAGE_IMPORT_DESCRIPTOR
这个结构体中记录着PE
文件要导入那些库文件
执行一个普通程序往往需要导入多个库,导入多少库就存在多少个IMAGE_IMPOET_DESCRIPTOR
结构体
Import:导入 向库提供服务(函数)
Export:导出 从库向其他PE文件提供服务(函数)
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;
- OriginalFirstThunk INT(import name table)的地址(RVA)
- Name 库名称字符串的地址(RVA)
- FirstThunk IAT的地址(RVA)
PE头中提到的
Table
即指数组INT与IAT是长整型数组,以NULL结束
INT中各元素的值为
IMAGE_IMPORT_BY_NAME
结构体指针(有时IAT也拥有相同的值)INT与IAT的大小相同
INT与IAT指向的地址可能不同(会有很多变形的PE文件)
PE装载器把导入函数输入至IAT的顺序
-
读取IID(image_import_descriptor)的Name成员,获取库名称字符串(“kernel32.dll”)
-
装载相应库
->LoadLibrary(“kernel32.dll”)
-
读取IID的OriginalFirstThunk成员,获取INT地址
-
逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
-
使用IMAGE_IMPORT_BY_NAME的Hint或Name项,获取相应函数的起始地址
->GetProcAddress(“GetCurrentThreadld”)
-
读取IID的FirstThunk(IAT)成员,获得IAT地址
-
将上面获得的函数地址填入相应的IAT数组值
-
重复以上步骤4-7 直到INT结束(遇到NULL)
寻找IMAGE_IMPORT_DESCRIPTOR
这个东西不在PE头,而在PE体,但是查找其位置的信息在PE头中,IMAGE_ORPTIONAL_HEADER32_DATADIRECTORY[1].VirtualAddress
的值即是IMAGE_IMPORT_DESCRIPTOR
结构体数组的起始地址(RVA)
然后使用RVA TO RAW
的转换公式,计算文件偏移找到地址
EAT
在Windows中,库是为了方便其他程序调用而集中包含相关函数的文件(DLL/SYS)
win32api是最具代表性的库,其中kernel32.dll被称为最核心的库文件
EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数,也就是只有通过EAT才能准确求得从相应库中导出函数的起始地址
与前文讲解的IAT相对,PE文件内的特定结构体(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY
结构体
IAT的IMAGE_IMPORT_DESCRIPTOR结构体以数组形式存在,且拥有多个成员。这样是因为PE文件可以同时导入多个库
IMAGE_EXPORT_DIRECTORY
结构体的RVA就是IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress
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;
-
NumberOfFunctions 实际Export函数的个数
-
NumberOfNames Export函数中具名的函数个数
-
AddressOfFunctions Export函数地址数组(数组元素个数=NumberOfFunctions)
-
AddressOfNames 函数名称地址数组(数组元素个数=NumberOfNames)
-
AddressOfNameOridinals Ordinal地址数组(数组元素个数=NumberOfNames)
从库中获得函数地址的API为GetProcAddress()
函数
该API引用EAT来获取指定API的地址
GetProcAddress
原理
- 利用AddressOfNames成员转到函数名称数组
- 比较字符串查找指定的函数名称
- 利用
AddressOfNameOridinals
成员,转到orinal
数组 - 在
ordinal
数组中通过name_index
查找相应的ordinal
值 - 利用
AddressOfFunctions
成员转到EAT - 在EAT将刚刚求得的
oridinal
当做数组索引,获得指定函数的起始地址
kernel32.dll
中所有导出函数均有相应名称
导出函数也有一些函数没有名称(仅通过ordinal导出)
把Ordinal当做导出函数的固有编号就行了,因为有时候某些函数对外不会公布函数名,仅仅公开函数的固有编号