先简单了解一下,后期跟着"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下方

image-20240709221525439

由代码与数据混合而成,即使没有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个比较重要的成员

  1. 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
    
  2. NumberOfSections

    PE文件把代码,数据,资源等依据属性分类到各节中存储

    这个值指出了文件中存在的节区数量,值比大于0,如果定义的节区数量与实际情况不符,运行错误

  3. SizeOfOptionalHeader

    IMAGE_NT_HEADER结构体的最后一个成员为IAMGE_OPTIONAL_HEADER32结构体

    而这个值用于指出IMAGE_OPTIONAL_HEADER32结构体的长度

  4. 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;

文件运行必须包含指定的成员

  1. Magic

    IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10B

    IMAGE_OPTIONALHEADER64结构体时,Magic码为20B

  2. AddressOfEntryPoint

    持有EP的RVA值,指出程序最先执行的代码起始地址

  3. ImageBase

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

    EXE,dll等装载到用户内存的0x7FFFFFFF

    SYS文件被载入到内核内存的80000000-FFFFFFFF

    一般而言EXE的IMAGEBASE值为00400000

    DLL的IMAGEBASE值为10000000,也可以指定为其他值

    执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为IMAGEBASE+OEP

  4. SectionAliginment,FileAlignment

    PE文件的body部分划分为若干节区,这些节存储不同类型的数据

    FileAlignment指定了节区磁盘文件中的最小单位

    SectionAlignment指定了节区在内存中的最小单位

    磁盘文件或内存的节区大小必定为File/SecitonAlignment的整数倍

  5. SizeOfImage

    加载PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占的空间大小(文件大小与加载的内存大小是不一样的,会经过映射

  6. SizeOfHeader

    用来指出整个PE头的大小,也必须是 FileAlignment的整数倍

  7. Subsystem

    区别系统驱动文件(/sys)与普通的可执行文件(.dll,.exe)

    值为1 Driver文件 系统驱动(ntfs.sys)

    值为2 GUI文件 窗口应用程序(notepad.exe)

    值为3 CUI文件 控制台应用程序(cmd.exe)

  8. NumberOfRvaAndSizes

    用来指定DataDirectory(即IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)数组的个数。定义上是16,也可能不是16

  9. DataDirectory

    这些数据段包括但不限于导入表、导出表、资源、异常处理信息、调试信息等。

    IMAGE_DATA_DIRECTORY结构体通常包含两个字段:

    1. VirtualAddress:表示该数据段在进程虚拟地址空间
    2. Size:表示该数据段的大小,以字节为单位。
    image-20240709224937160

节区头

PE文件中的code,data,resource等按照属性分类存储在不同节区

把PE文件创建成多个节区的结构的好处是 可以保证程序的安全性

若把codedata放在一个节区中相互纠缠很容易引发安全问题

加入我们向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)

VirtualAddressPointerToRawData不带有任何只。由定义在IMAGE_OPTIONAL_HEADER32中的SectionAlignmentFileAlignment 确定

VirtualSizeSizeOfRawData一般具有不同的值,因为

磁盘中和内存中的节区大小是不一样的

  • 再了解一下Name字段,name成员不像C语言的字符串一样以NULL结束,可以放入任何值甚至填充NULL值

  • 术语IMAGE实际上指的的内存中的PE映像

RVA to RAW

RAW是磁盘中数据对文件的偏移

PE文件从磁盘到内存映射的内容

节中数据的RAW-节头的RAW=节中数据RVA-节头的RVA
RAW-PointerToRawData=RVA-VirtualAddress
RAW=RVA-VirtualAddress+PointerToRawData
image-20240710193542122

因此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的顺序

  1. 读取IID(image_import_descriptor)的Name成员,获取库名称字符串(“kernel32.dll”)

  2. 装载相应库

    ->LoadLibrary(“kernel32.dll”)

  3. 读取IID的OriginalFirstThunk成员,获取INT地址

  4. 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)

  5. 使用IMAGE_IMPORT_BY_NAME的Hint或Name项,获取相应函数的起始地址

    ->GetProcAddress(“GetCurrentThreadld”)

  6. 读取IID的FirstThunk(IAT)成员,获得IAT地址

  7. 将上面获得的函数地址填入相应的IAT数组值

  8. 重复以上步骤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)

image-20240710224952220

从库中获得函数地址的API为GetProcAddress()函数

该API引用EAT来获取指定API的地址

GetProcAddress原理

  1. 利用AddressOfNames成员转到函数名称数组
  2. 比较字符串查找指定的函数名称
  3. 利用AddressOfNameOridinals成员,转到orinal数组
  4. ordinal数组中通过name_index查找相应的ordinal
  5. 利用AddressOfFunctions成员转到EAT
  6. 在EAT将刚刚求得的oridinal当做数组索引,获得指定函数的起始地址

kernel32.dll中所有导出函数均有相应名称

导出函数也有一些函数没有名称(仅通过ordinal导出)

把Ordinal当做导出函数的固有编号就行了,因为有时候某些函数对外不会公布函数名,仅仅公开函数的固有编号