PE文件解析


简介

PE文件结构分析,包含Image_Dos_Header,Image_NT_Header(Signature,FileHeader,OptionalHeader),Image_Section_Header。

其中着重描述导出表、导入表、重定位表。

头部详解

头部详解图

在image_optional_header结构中,sizeofimage通常指的是某个特定字段的大小。在Windows Portable Executable (PE)文件格式中,SizeOfImage是整个映像文件(image file)的内存映像的大小,它包括了加载时所有的部分(如代码段、数据段、导入表等)的大小。这是整个PE文件在内存中占用的大小,通常在PE文件被加载到内存后,操作系统根据此值来分配足够的空间。

在image_optional_header结构中,SizeOfHeaders字段表示PE文件所有头部信息的总大小,这包括了:

  1. DOS头(DOS Header),即 IMAGE_DOS_HEADER。
  2. PE签名(PE Signature),通常是4字节的 "PE\0\0"。
  3. 文件头(File Header),即 IMAGE_FILE_HEADER。
  4. 可选头(Optional Header),即 IMAGE_OPTIONAL_HEADER,包含更多详细的加载信息。
  5. 节表(Section Headers),即 IMAGE_SECTION_HEADER,描述了PE文件中的各个节(段)的起始位置、大小等。

SizeOfHeaders反映的是这些头部信息在PE文件中的总大小,并且通常是按文件对齐值(FileAlignment)对齐的。

IMAGE_DOS_HEADER

IMAGE_DOS_HEADER 是 Windows 可执行文件格式中的一个结构体,它是 DOS 头部(DOS Header)的一部分,位于可执行文件的最前面。这个结构体包含了一些用于启动程序和处理文件的基本信息。下面是 IMAGE_DOS_HEADER 结构体的详细内容和解释:(都是占两个字节)

typedef struct _IMAGE_DOS_HEADER 
{
    WORD   e_magic;               // 魔法数字,用于标识这是一个MZ执行文件
    WORD   e_cblp;                // 指向最后一页的偏移量
    WORD   e_cp;                 // 程序的重定位项数量
    WORD   e_crlc;                // 文件中的大小(以页为单位)
    WORD   e_cparhdr;             // 程序头部的大小
    WORD   e_minalloc;            // 程序运行时最小分配的段落数
    WORD   e_maxalloc;            // 程序运行时最大分配的段落数
    WORD   e_ss;                  // 程序开始时的堆栈段选择子
    WORD   e_sp;                  // 程序开始时的堆栈指针
    WORD   e_csum;               // 程序的校验和
    WORD   e_ip;                  // 程序开始时的指令指针
    WORD   e_cs;                  // 程序开始时的代码段选择子
    WORD   e_lfarlc;              // 文件中重定位表的位置
    WORD   e_ovno;                // 覆盖号
    // ... 接下来的是用于覆盖的保留字段 ...
    WORD   e_res[4];              // 保留字段,可用于应用程序或操作系统使用
    WORD   e_oemid;               // OEM标识符,用于操作系统识别不同的OEM程序
    WORD   e_oeminor;             // OEM修订号
    // ... 接下来的是用于覆盖的保留字段 ...
    WORD   e_res2[10];             // 保留字段,可用于操作系统或应用程序的扩展
    WORD   e_lfanew;               // 新的文件头部位置,从该位置开始是NT头部或PE头部
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_DOS_HEADER 结构体的总大小正好是 64 字节。这个结构体位于所有 Windows 可执行文件的最开始部分,用于向后兼容 MS-DOS 系统。在现代的操作系统中,这个结构体的存在主要是为了保持与旧软件的兼容性,并且确保文件可以被正确地解析和加载。这个结构体是Windows可执行文件格式的一部分,它在文件的最前面,用于向后兼容DOS系统,并为程序的加载和执行提供必要的信息。随着操作系统的发展,这个结构体的作用逐渐减少,但它仍然是Windows可执行文件格式的一个标准部分。

修改mz标记会无法打开应用程序。

修改pe标记会无法打开应用程序。

IMAGE_DOS_HEADER只对16位程序有用,32、64位程序只使用第一个和最后一个成员。

IMAGE_NT_HEADER

文件起始 0x5A4D IMAGE_DOS_HEADER
大小0x40
偏移
Signature PE标记 IMAGE NT HEADER
标准PE头 IMAGE_FILE_HEADER
扩展PE头 IMAGE_OPTIONAL_HEADER

IMAGE_NT_HEADERS 是 Windows PE(Portable Executable)文件格式中的一个关键结构,它紧随 DOS 头部之后,提供了文件的运行时信息。这个结构是 PE 文件的 NT(New Technology)头部,包含了操作系统加载和执行程序所需的重要信息。

下面是 IMAGE_NT_HEADERS 结构的一般定义,这个结构定义在 winnt.h 头文件中:

//32位大小:0xF8  248字节
//64位大小:0x108 264字节
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                // 0x00004550, 用于标识 PE 文件格式
    IMAGE_FILE_HEADER FileHeader;     // 文件头部,包含文件的机器类型、节的数量等信息 标准PE头 大小0x14  20个字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头部,包含程序的入口点、基址、子系统类型等信息 扩展PE头 32位下大小0xE0
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;                // 0x00004550, 用于标识 PE 文件格式
    IMAGE_FILE_HEADER FileHeader;     // 文件头部 标准PE头 大小0x14  20个字节
    IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 可选头部 扩展PE头 64位下大小0xF0
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

Signature

Signature:一个 DWORD 类型的字段,其值固定为 0x00004550,(“P”“E”“0”“0”)这是 PE 文件的标志,用于确认文件是按照 PE 格式构建的。否则无法正常启动。

IMAGE_FILE_HEADER

IMAGE_FILE_HEADER 是 Windows PE 文件格式中的一个结构,它提供了一系列关于可执行文件或 DLL 的基本信息。这个结构位于 IMAGE_NT_HEADERS 结构中,紧随 Signature 字段之后。下面是 IMAGE_FILE_HEADER 结构的完整定义,以及每个字段的详细解释:

typedef struct _IMAGE_FILE_HEADER {//大小20字节
    WORD   Machine ;
     // Machine: 指定文件是为哪种类型的CPU架构编译的。
    // 例如,IMAGE_FILE_MACHINE_I386代表32位x86架构,而IMAGE_FILE_MACHINE_AMD64代表64位x64架构。 
    WORD   NumberOfSections ;
     // NumberOfSections: 表示文件中包含的节(section)的数量。
    // 节是PE文件中的一种逻辑结构,通常包含代码、数据或其他类型的信息。 
    DWORD  TimeDateStamp ;
     // TimeDateStamp: 记录文件创建或最后修改的时间戳。
    // 这个时间戳通常是基于UTC时间的,以秒为单位,从1970年1月1日开始计算。 
    DWORD PointerToSymbolTable;
     // PointerToSymbolTable: 指向文件中符号表的起始位置的偏移量。
    // 符号表包含了程序中所有的符号名称和它们在文件中的地址,用于调试和符号解析。 
    DWORD NumberOfSymbols;
     // NumberOfSymbols: 表示符号表中符号的总数。
    // 每个符号通常对应一个全局变量、函数或其他可导出的实体。 
    WORD   SizeOfOptionalHeader ;
     // SizeOfOptionalHeader: 指定可选头部(IMAGE_OPTIONAL_HEADER)的大小,单位是字节。 
     // 可选头部包含了更多关于如何和在哪里加载程序的信息。 
    WORD   Characteristics ;
     // Characteristics: 一系列标志位,描述文件的特性。
    // 例如,它可以指示文件是否是可执行文件、DLL、是否包含调试信息等。 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  1. Machine (2 字节): 这个字段指定了目标平台的机器类型和 CPU 架构。例如,IMAGE_FILE_MACHINE_I386 表示 x86 架构,IMAGE_FILE_MACHINE_AMD64 表示 x64 架构。这个字段的值是 IMAGE_FILE_MACHINE 枚举类型的一部分。

    // 定义 IMAGE_FILE_HEADER 结构体中的 Machine 字段的可能值。
    // 这些值指示了可执行文件或 DLL 针对的 CPU 架构类型。
    typedef enum _IMAGE_FILE_MACHINE {
        IMAGE_FILE_MACHINE_UNKNOWN = 0x0000, // 未知机器类型
        IMAGE_FILE_MACHINE_I386 = 0x014c,     // Intel 386 或兼容的 32 位 x86 架构
        IMAGE_FILE_MACHINE_R4000 = 0x0162,     // MIPS R4000 架构
        IMAGE_FILE_MACHINE_WCEMIPSV2 = 0x0174, // MIPS little-endian WCE v2
        IMAGE_FILE_MACHINE_ALPHA = 0x0184,     // DEC Alpha 架构
        IMAGE_FILE_MACHINE_SH3 = 0x01a2,       // Hitachi SH3 架构
        IMAGE_FILE_MACHINE_SH3DSP = 0x01a3,    // Hitachi SH3 DSP 架构
        IMAGE_FILE_MACHINE_SH4 = 0x01a6,       // Hitachi SH4 架构
        IMAGE_FILE_MACHINE_ARM = 0x01c0,       // ARM 架构
        IMAGE_FILE_MACHINE_ARMNT = 0x01c4,      // ARM with Windows NT executable format
        IMAGE_FILE_MACHINE_ARM64 = 0xaa64,     // ARM64 架构
        IMAGE_FILE_MACHINE_MIPS16 = 0x0266,    // MIPS16 架构
        IMAGE_FILE_MACHINE_MIPSFPU = 0x0366,    // MIPS with FPU 架构
        IMAGE_FILE_MACHINE_MIPSFPU16 = 0x0466,  // MIPS16 with FPU 架构
        IMAGE_FILE_MACHINE Trigram = 0x0522,   // Tridium Co. Trigram 架构
        IMAGE_FILE_MACHINE_CEF = 0x0CEF,      // CEF 架构
        IMAGE_FILE_MACHINE_EBC = 0x0EBC,      // EFI 引导代码
        IMAGE_FILE_MACHINE_AMD64 = 0x8664,     // AMD64(x64)架构
        IMAGE_FILE_MACHINE_M32R = 0x9041,      // M32R 架构
        // ... 其他可能的值 ...
    
        // 其他值可以在这里定义,具体取决于新的 CPU 架构或平台的出现。
    } IMAGE_FILE_MACHINE, *PIMAGE_FILE_MACHINE;
  2. NumberOfSections (2 字节): 这个字段表示文件中节(section)的数量。每个节通常包含程序代码、数据或其他类型的信息。这个字段的值告诉操作系统或加载器有多少个节需要加载和解析。

  3. TimeDateStamp (4 字节): 这个字段记录了文件的创建或最后修改的时间戳。它通常以 Unix 时间戳的形式表示,即从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间的秒数。这个字段可以用来确定文件的版本和更新历史。

  4. PointerToSymbolTable (4 字节): 这个字段是符号表的起始地址,以文件中的字节偏移量表示。符号表包含了程序中所有符号(如函数名和变量名)的列表和它们在文件中的地址。这个字段在较新的 PE 文件中通常被设置为 0,因为符号信息通常存储在节中。

  5. NumberOfSymbols (4 字节): 这个字段表示符号表中的符号数量。每个符号通常对应程序中的一个函数、变量或其他命名实体。这个字段有助于加载器和调试器查找和解析符号信息。

  6. SizeOfOptionalHeader (2 字节): 这个字段指定了可选头部(IMAGE_OPTIONAL_HEADER)的大小,以字节为单位。可选头部包含了更多的程序加载和执行所需的信息,如程序的入口点、基址、子系统类型等。

  7. Characteristics (2 字节): 这个字段包含了一系列的标志位,用于描述文件的特性。例如,IMAGE_FILE_RELOCS_STRIPPED 表示文件已经被剥离了重定位信息,IMAGE_FILE_DLL 表示文件是一个动态链接库(DLL)。这些标志位是 IMAGE_FILE_FLAGS 枚举类型的一部分。

    • 下面是 Characteristics 字段的数据位、常量符号、常量值以及解释的表格:

      数据位 常量符号 常量值 (十六进制) 解释
      0 IMAGE_FILE_RELOCS_STRIPPED 0x0001 文件已经剥离了重定位信息,不包含用于动态链接的重定位和线缆信息。
      1 IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 文件是一个可执行图像,即它是一个可执行文件而不是一个 DLL。
      2 IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 文件中的行号信息已被移除。
      3 IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 文件中的本地符号信息已被移除。
      4 IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 文件的节可以被操作系统更积极地修剪,以减少工作集的大小。
      5 IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 文件可以处理大于 2GB 的地址空间。
      6 IMAGE_FILE_32BIT_MACHINE 0x0100 文件是为 32 位系统编译的。
      7 IMAGE_FILE_DEBUG_STRIPPED 0x0200 文件已经被调试剥离,即调试信息已经被移除。
      8 IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 文件可以从交换文件中运行,这是一种优化技术。
      9 IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 文件可以从网络位置运行,并且应该从交换文件中加载。
      10 IMAGE_FILE_DLL 0x2000 文件是一个动态链接库(DLL)。
      11 IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 文件仅适用于基于 Windows 的系统,而不是基于 MS-DOS 的系统。
      12 IMAGE_FILE_BYTES_REVERSED_LO 0x8000 这个标志位通常不被使用,它曾用于表示文件的低位字节是反序的。

IMAGE_FILE_HEADER 结构的大小是 20 字节(不包括可选头部)。这个结构是 PE 文件格式的核心组成部分,它提供了操作系统和加载器需要的基本信息,以正确地加载和执行程序。

IMAGE_OPTIONAL_HEADER

IMAGE_OPTIONAL_HEADER 是 Windows PE 文件格式中的一个结构,它提供了关于可执行文件或 DLL 的附加信息,这些信息对于操作系统加载和执行程序至关重要。这个结构位于 IMAGE_NT_HEADERS 结构中,紧随 IMAGE_FILE_HEADER 之后。IMAGE_OPTIONAL_HEADER 的具体定义取决于操作系统的位数(32位或64位),因此有两个版本:IMAGE_OPTIONAL_HEADER32 和 IMAGE_OPTIONAL_HEADER64。

IMAGE_OPTIONAL_HEADER32

IMAGE_OPTIONAL_HEADER32 是 Windows PE 格式中用于 64 位可执行文件的可选头部结构。它提供了关于可执行文件或 DLL 的附加信息,这些信息对于操作系统加载和执行程序至关重要。以下是 IMAGE_OPTIONAL_HEADER32 的详细定义,以及一个表格,其中整理了每个字段的常量值、解释和大小:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD Magic;                           // 0x00, 2字节, 标识PE文件的类型
    BYTE MajorLinkerVersion;               // 0x02, 1字节, 链接器的主版本号
    BYTE MinorLinkerVersion;               // 0x03, 1字节, 链接器的次版本号
    DWORD SizeOfCode;                     // 0x04, 4字节, 代码段的大小
    DWORD SizeOfInitializedData;         // 0x08, 4字节, 初始化数据段的大小
    DWORD SizeOfUninitializedData;       // 0x0c, 4字节, 未初始化数据段的大小
    DWORD AddressOfEntryPoint;             // 0x10, 4字节, 程序入口点的地址
    DWORD BaseOfCode;                     // 0x14, 4字节, 代码段的基地址
    DWORD BaseOfData;                     // 0x18, 4字节, 数据段的基地址
    DWORD ImageBase;                      // 0x1c, 4字节, 映像基地址
    DWORD SectionAlignment;               // 0x20, 4字节, 节对齐的地址大小
    DWORD FileAlignment;                  // 0x24, 4字节, 文件对齐的地址大小
    WORD MajorOperatingSystemVersion;      // 0x28, 2字节, 操作系统的主要版本号
    WORD MinorOperatingSystemVersion;      // 0x2a, 2字节, 操作系统的次要版本号
    WORD MajorImageVersion;                // 0x2c, 2字节, 映像文件的主要版本号
    WORD MinorImageVersion;                // 0x2e, 2字节, 映像文件的次要版本号
    WORD MajorSubsystemVersion;             // 0x30, 2字节, 子系统的主要版本号
    WORD MinorSubsystemVersion;             // 0x32, 2字节, 子系统的次要版本号
    DWORD Win32VersionValue;                // 0x34, 4字节, Windows特定版本的保留值
    DWORD SizeOfImage;                     // 0x38, 4字节, 映像的大小
    DWORD SizeOfHeaders;                   // 0x3c, 4字节, 所有头部的大小
    DWORD CheckSum;                        // 0x40, 4字节, 文件的校验和
    WORD Subsystem;                        // 0x44, 2字节, 程序运行的子系统类型
    WORD DllCharacteristics;              // 0x46, 2字节, DLL特性标志
    DWORD SizeOfStackReserve;               // 0x48, 4字节, 为程序栈保留的内存大小
    DWORD SizeOfStackCommit;                // 0x4c, 4字节, 初始提交的程序栈大小
    DWORD SizeOfHeapReserve;                // 0x50, 4字节, 为堆保留的内存大小
    DWORD SizeOfHeapCommit;                 // 0x54, 4字节, 初始提交的堆大小
    DWORD LoaderFlags;                      // 0x58, 4字节, 加载器使用的保留位
    DWORD NumberOfRvaAndSizes;              // 0x5c, 4字节, 数据目录的数量
    _IMAGE_DATA_DIRECTORY DataDirectory[16];  // 0x60, 16个数据目录,每个8字节,大小为128个字节
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

请注意,IMAGE_OPTIONAL_HEADER32 结构的末尾包含一个数据目录数组,每个数据目录包含 RVA 和大小信息。这些数据目录提供了关于 PE 文件中各种数据结构位置和大小的信息,例如导出表、导入表、资源等。这个数组的长度由 NumberOfRvaAndSizes 字段指定。

从00000000到FFFFFFFF,4GB内存空间,80000000到FFFFFFFF是内核空间使用。

虚拟内存地址(Virtual Address)= 基地址(ImageBase)+ 相对虚拟地址(RVA)relative virtual address//可以理解为偏移

文件偏移地址(FOA - file offset address)

在32位操作系统中,虚拟内存空间被划分为两个主要部分:低2G(Low 2GB)和高2G(High 2GB)。这种划分是基于32位系统能够支持的最大虚拟内存空间,即4GB(2^32字节)。

  1. 低2G (Low 2GB):
    • 低2G指的是虚拟地址空间的低端,从 0x00000000 到 0x7FFFFFFF。
    • 这部分地址空间通常被分配给用户空间,允许用户程序和应用程序运行。
    • 用户空间的程序无法直接访问内核空间的内存,这样可以提供一定程度的隔离和保护,防止用户程序干扰内核操作和系统稳定性。
    • 在这个地址范围内,程序可以安全地分配内存、执行代码和访问它们自己的资源。
  2. 高2G (High 2GB):
    • 高2G指的是虚拟地址空间的高端,从 0x80000000 到 0xFFFFFFFF。
    • 这部分地址空间通常被分配给内核空间,用于操作系统内核、设备驱动程序和其他系统级服务。
    • 内核空间拥有更高的权限级别,可以执行更多的指令集和访问硬件资源,以及管理系统级别的数据结构和任务。
    • 操作系统内核使用这个地址范围来执行系统调用、内存管理、进程调度和硬件交互等任务。

在实际的内存管理中,操作系统使用虚拟内存技术来模拟更大的内存空间,即使物理内存可能远小于4GB。虚拟内存允许操作系统通过分页机制将物理内存和硬盘上的交换空间结合起来,从而有效地管理内存资源。

需要注意的是,"低2G"和"高2G"的概念主要与32位系统相关。在64位系统中,由于支持的虚拟地址空间大大增加(达到16EB),这种划分不再适用,操作系统可以更灵活地管理内存空间。在64位系统中,内核空间和用户空间的划分不再局限于特定的2GB范围,而是可以利用整个虚拟地址空间。

字段名称 大小 描述
Magic 2字节 魔数,用于区分PE格式的类型IMAGE_NT_OPTIONAL_HDR64_MAGIC (0x20B) 表示 64 位 PE 文件。
MajorLinkerVersion 1 字节 链接器的主版本号。
MinorLinkerVersion 1 字节 链接器的次版本号。
SizeOfCode 4 字节 代码段的大小。
SizeOfInitializedData 4 字节 初始化数据段的大小。
SizeOfUninitializedData 4 字节 未初始化数据段的大小。
AddressOfEntryPoint 4 字节 入口点的 RVA (相对虚拟地址)。
BaseOfCode 4 字节 代码段的基地址。
BaseOfData 4 字节 数据段的基地址。
ImageBase基地址 4 字节 期望的加载基址。
SectionAlignment 4 字节 节的对齐大小。
FileAlignment 4 字节 文件的对齐大小。
MajorOperatingSystemVersion 2 字节 操作系统的主版本号。
MinorOperatingSystemVersion 2 字节 操作系统的次版本号。
MajorImageVersion 2 字节 映像的主版本号。
MinorImageVersion 2 字节 映像的次版本号。
MajorSubsystemVersion 2 字节 子系统的主要版本号。
MinorSubsystemVersion 2 字节 子系统的次要版本号。
Win32VersionValue 4 字节 用于 Windows 特定版本的值。
SizeOfImage 4 字节 映像大小(以字节为单位)。
SizeOfHeaders 4 字节 头的大小(包括 DOS 头、PE 头和节头)。
CheckSum 4 字节 文件校验和。
Subsystem 2 字节 子系统类型(例如 Windows, Console, POSIX)。
DllCharacteristics 2 字节 DLL 特性(例如是否允许多个程序共享)。
SizeOfStackReserve 4 字节 堆栈保留的大小。
SizeOfStackCommit 4 字节 堆栈提交的大小。
SizeOfHeapReserve 4 字节 堆保留的大小。
SizeOfHeapCommit 4 字节 堆提交的大小。
LoaderFlags 4 字节 加载器标志。
NumberOfRvaAndSizes 4 字节 数据目录的数量。
  1. ImageBase 字段的值通常在文件创建时由编译器或链接器设置,并在文件加载时由操作系统使用。操作系统会尝试将文件加载到指定的基址。然而,如果指定的基址与其他已加载的模块冲突,操作系统可能会选择一个不同的基址来加载文件,这个过程称为基址重定位(Base Relocation)。 在实际应用中,ImageBase​ 通常设置为一个较高的地址,以便为应用程序提供足够的地址空间,同时避免与系统和其他关键模块的地址空间冲突。例如,许多应用程序的 ImageBase​ 被设置为 0x40000000​。 ​ImageBase​ 只是一个期望的基址,实际加载的基址可能会因为内存碎片、操作系统策略或其他因素而有所不同。此外,ImageBase​ 也可以在程序运行时通过修改程序的加载方式来改变,例如使用 Windows 的 LoadLibrary​ 函数加载 DLL 时可以指定一个新的基址。
  2. SectionAlignment 字段是 IMAGE_OPTIONAL_HEADER 结构的一部分,它指定了 PE(Portable Executable)或 COFF(Common Object File Format)文件中节(section)的对齐要求。这个字段定义了文件中各个节应该如何被映射到内存中的地址。在 32 位和 64 位的 PE 文件中,SectionAlignment 字段都是 ULONGLONG 类型,在 32 位系统中通常是一个 DWORD 类型。 节对齐是指将节的起始地址按照某个特定的大小对齐,这个大小就是 SectionAlignment​ 字段的值。对齐的目的是为了提高内存访问的效率,因为许多处理器在处理对齐的内存块时速度更快。此外,某些硬件平台要求数据按照特定的边界对齐,否则可能会导致硬件异常或性能下降。 例如,如果 SectionAlignment​ 设置为 0x1000​(即 4096),这意味着每个节的起始地址必须是 4096 的倍数。如果节的实际大小不是 SectionAlignment​ 的倍数,那么操作系统会在节的末尾添加足够的填充字节(通常是零),以确保整个节的大小是 SectionAlignment​ 的倍数。 在实际应用中,SectionAlignment​ 的值通常取决于操作系统和处理器的要求。例如,Windows 系统上常见的节对齐大小有 4096 字节(4KB)、8192 字节(8KB)等。这个字段是确保程序能够高效运行在目标硬件上的重要参数之一。
  3. AddressOfEntryPoint 是 IMAGE_OPTIONAL_HEADER 结构中的一个字段,它指定了 PE(Portable Executable)文件的入口点。这个入口点是程序开始执行的起始位置,通常是程序的 main 函数或者操作系统调用的第一个执行点。 32 位 PE 文件中,AddressOfEntryPoint 是一个 DWORD 类型,表示一个 32 位的相对虚拟地址(RVA)。在 64 位 PE 文件中,它是一个 ULONGLONG 类型,表示一个 64 位的 RVA。这个地址是相对于 ImageBase(期望的加载基址)的偏移量。 当操作系统加载 PE 文件到内存中时,它会根据 ImageBase​ 将 AddressOfEntryPoint​ 转换为实际的内存地址。这个地址是程序执行的起点,操作系统会从这个地址开始执行程序的代码。请注意,AddressOfEntryPoint​ 只是一个 RVA,实际的物理地址是在加载时由操作系统计算得出的。这个 RVA 通常是相对于 PE 文件中的某个节的起始地址,而 ImageBase​ 是整个 PE 文件在内存中的起始地址。 因此,实际的入口点地址可以通过将 ImageBase​ 和 AddressOfEntryPoint​(作为偏移量)相加来计算得出。在程序的开发和链接过程中,链接器负责确定并设置 AddressOfEntryPoint​ 的值,以确保程序能够正确地被操作系统加载和执行。

对于 DLL(动态链接库)和驱动程序(设备驱动或内核驱动),AddressOfEntryPoint 字段在 PE(Portable Executable)文件的 IMAGE_OPTIONAL_HEADER 结构中仍然扮演着重要角色,但其具体含义和使用方式与可执行文件略有不同。

  • DLL:
    • DLL 的 AddressOfEntryPoint 通常指向 DLL 的初始化例程,这个例程在 DLL 被加载到进程的地址空间时被调用。
    • 这个入口点函数通常被称为 DllMain 或类似的名称,它负责执行 DLL 初始化所需的任务,如设置结构、分配资源或注册回调函数。
    • 如果 DLL 没有特定的初始化例程,AddressOfEntryPoint 可以设置为 0,表示没有入口点。
  • 驱动程序:
    • 对于设备驱动程序(如 Windows 的 .sys 文件),AddressOfEntryPoint 可能指向驱动程序的初始化例程,这个例程在驱动程序被加载到内核空间时被调用。
    • 对于内核驱动程序,这个入口点通常是驱动程序的 DriverEntry 函数,它是驱动程序开始执行的第一个函数。
    • 在某些情况下,驱动程序可能不需要在加载时立即执行代码,因此 AddressOfEntryPoint 可能不被使用或者设置为 0。
关于节和文件对齐的理解

在 PE(Portable Executable)文件格式中,SectionAlignment 和 FileAlignment 是 IMAGE_OPTIONAL_HEADER 结构中的两个字段,它们定义了节(section)和文件的对齐要求。这两个字段对于操作系统如何加载和映射文件到内存中具有重要意义。

  1. 节的对齐(SectionAlignment):
    • SectionAlignment 指定了文件中各个节在内存中的对齐边界。节是 PE 文件中的基本单位,包含了程序的代码、数据、资源等。
    • 对齐意味着每个节的起始地址必须是 SectionAlignment 指定大小的倍数。例如,如果 SectionAlignment 是 0x1000(4096字节),那么所有节的起始地址必须是 4096 字节的倍数。
    • 这种对齐可以提高内存访问效率,因为许多处理器对齐的内存访问速度更快,而且某些硬件平台可能要求数据按照特定的边界对齐。
  2. 文件的对齐(FileAlignment):
    • FileAlignment 指定了文件在磁盘上的对齐要求。它定义了文件中数据的存储对齐边界,通常用于减少磁盘寻址时间,提高文件读写效率。
    • 文件对齐通常与磁盘扇区大小有关。例如,如果磁盘扇区大小是 512 字节,那么 FileAlignment 可能会设置为 512 字节或其倍数,以确保文件数据按照扇区对齐存储。
    • 在某些情况下,FileAlignment 也可能与操作系统的虚拟内存分页大小有关,例如 4096 字节(4KB)。

在实际应用中,SectionAlignment 和 FileAlignment 的值通常由编译器或链接器根据目标平台的硬件特性和操作系统的要求来设置。这些对齐要求有助于确保程序能够高效地运行,同时减少磁盘 I/O 操作和内存访问的开销。

对齐例子

让我们通过一个具体的例子来解释节的对齐(SectionAlignment)和文件的对齐(FileAlignment)。

假设我们有一个简单的 PE 文件,它包含两个节:一个代码节(.text)和一个数据节(.data)。操作系统和硬件平台要求数据按 4096 字节(4KB)对齐。

  1. 节的对齐(SectionAlignment):
    • 假设 SectionAlignment 被设置为 0x1000(即 4096 字节)。
    • 现在,代码节 .text 的实际大小是 3000 字节,但因为它需要在内存中按 4096 字节对齐,所以操作系统会在 .text 节后添加 4096 - 3000 = 1026 字节的填充(通常是零),使得下一个节的起始地址是 4096 字节的倍数。
    • 接着,数据节 .data 也按照同样的对齐要求处理。如果 .data 的大小是 5000 字节,操作系统会在 .data 节后添加 4096 - 5000 % 4096 = 1024 字节的填充,以确保下一个节(如果有的话)的起始地址也是 4096 字节的倍数。
  2. 文件的对齐(FileAlignment):
    • 假设 FileAlignment 也被设置为 0x1000(即 4096 字节)。
    • 当 PE 文件被写入磁盘时,操作系统会确保每个节的起始位置都是 4096 字节的倍数。这意味着,即使节的大小不是 4096 字节的倍数,文件的大小也会被填充到最接近的 4096 字节的倍数,以确保整个文件在磁盘上也是按 4096 字节对齐的。
    • 这种对齐有助于提高磁盘 I/O 性能,因为磁盘读写操作可以更高效地以块为单位进行。

在这个例子中,SectionAlignment 和 FileAlignment 的对齐要求确保了内存中的数据处理和磁盘上的文件存储都遵循了硬件和操作系统的最佳实践。这种对齐可以减少 CPU 缓存未命中的可能性,提高内存访问速度,同时也优化了磁盘 I/O 操作,从而提高了程序的整体性能。

IMAGE_DATA_DIREACTORY
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;     /**指向某个数据的相对虚拟地址   RAV  偏移0x00**/              
    DWORD   Size;               /**某个数据块的大小                偏移0x04**/      
}  *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

IMAGE_OPTIONAL_HEADER结构体最后一个成员是数组结构,大小为16,每个元素都是一个IMAGE_DATA_DIRECTORY结构体

在这个数据目录结构体中只有两个成员VirtualAddress和Size,这两个成员的含义比较简单,VirtualAddress指定了数据块的相对虚拟地址(RVA)。Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该类型数据一个数据项的大小。这两个成员(主要是VirtualAddress)成为了定位各种表的关键,所以一定要知道每个数组元素所指向的数据块类型,以下表格就是它的对应关系:

DATA_DIRECTORY结构体成员一览

// 目录项

#define  IMAGE_DIRECTORY_ENTRY_EXPORT           0   // 导出目录
#define  IMAGE_DIRECTORY_ENTRY_IMPORT           1   // 导入目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // 安全目录
#define  IMAGE_DIRECTORY_ENTRY_BASERELOC        5   // 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 调试目录
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 用法)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 架构特定数据
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // 全局指针 (GP) 的 RVA
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // 线程局部存储 (TLS) 目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // 加载配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // 头部中的绑定导入目录
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // 导入地址表 (IAT)
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // 延迟加载导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM 运行时描述符

IMAGE_SECTION_HEADER

标准PE头IMAGE_FILE_HEADER中有个NumberOfSections记录了有多少个节

IMAGE_SECTION_HEADER定义

IMAGE_SECTION_HEADER 是 Windows PE 文件格式中用于描述各个节(section)的属性和信息的结构体。在 PE 文件中,节是文件被分割成的逻辑单元,每个节包含了程序的不同部分,如代码、数据、资源等。IMAGE_SECTION_HEADER 通常紧随 IMAGE_NT_HEADERS 结构之后,为每个节提供了详细的元数据。

以下是 IMAGE_SECTION_HEADER 结构体的定义,以及每个字段的详细解释:

#define IMAGE_SIZEOF_SECTION_HEADER       40
#define IMAGE_SIZEOF_SHORT_NAME           8

typedef struct _IMAGE_SECTION_HEADER {
    char  Name [8];                    // 节的名称,通常是 8 字节长[IMAGE_SIZEOF_SHORT_NAME]
    union {
        DWORD PhysicalAddress;       // 节的物理地址(低 32 位)
        DWORD  VirtualSize ;           // 节的虚拟大小(低 32 位)
    } Misc;
    DWORD  VirtualAddress ;             // 节的虚拟地址(低 32 位)
    DWORD  SizeOfRawData ;               // 节在文件中的原始数据大小
    DWORD  PointerToRawData ;           // 指向节在文件中原始数据的指针
    DWORD PointerToRelocations;       // 指向节的重定位表的指针
    DWORD PointerToLinenumbers;        // 指向节的行号表的指针
    WORD  NumberOfRelocations;        // 节中重定位项的数量
    WORD  NumberOfLinenumbers;         // 节中行号项的数量
    DWORD Characteristics;             // 节的特性和标志
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • Name: 一个 8 字节的数组,用于存储节的名称。这个名称通常是节的用途的简短描述,如 .text 用于存储代码,.data 用于存储初始化的数据。

    在 IMAGE_SECTION_HEADER 结构中,Name 字段是一个 8 字节的数组,用于存储节(section)的名称。这个名称通常是对节内容的简短描述,它对于理解文件的结构和调试程序非常有用。在实际的 PE 文件中,Name 字段可能包含各种常见的名称,这些名称通常遵循一定的命名约定。

    以下是一些在 PE 文件中常见的 Name 值示例:

    .text: 通常用于存储程序的可执行代码。

    .data: 用于存储程序的初始化全局变量和静态变量。

    .rdata 或 .rodata: 用于存储只读数据,如字符串常量和全局常量。

    .bss: 用于存储未初始化的全局变量和静态变量。在文件中可能不占用实际空间,但在内存中会被分配空间并初始化为零。

    .pdata: 可能用于存储程序的动态调试信息。

    .xdata: 可能用于存储程序的异常处理信息。

    .debug: 用于存储调试信息,这些信息可能包括源代码行号、符号名称等。

    _tls: 用于存储线程局部存储(Thread Local Storage)的数据。

    .reloc: 用于存储程序的重定位信息。

    这些名称是按照 PE 文件格式和编程惯例约定的,它们有助于程序员和工具链识别和处理文件中的不同部分。在创建或修改 PE 文件时,开发者和链接器会根据节的用途来设置合适的 Name 值。

    请注意,Name 字段是大小写不敏感的,且操作系统和加载器在处理 PE 文件时会根据这些名称来识别节的类型和用途。此外,Name 字段的长度是固定的 8 字节,如果节名称小于 8 字节,剩余的空间通常会用空字符(\0)填充。

  • Misc: 一个联合体,包含两个字段 PhysicalAddress 和 VirtualSize。PhysicalAddress 通常不使用,而 VirtualSize 表示节在虚拟地址空间中的大小。如果节的大小是 32 位的,那么 VirtualSize 可能与 SizeOfRawData 相同,或者在某些情况下,VirtualSize 可能大于 SizeOfRawData(例如,当节包含未初始化的数据时)。

    在 IMAGE_SECTION_HEADER 结构中,Misc 字段是一个联合体(union),它包含两个成员:PhysicalAddress 和 VirtualSize。这两个成员共享同一块内存空间,但用于不同的上下文。

    由于这是一个联合体,它的大小由最大的成员决定。在这种情况下,PhysicalAddress 和 VirtualSize 都是 DWORD 类型,DWORD 通常是一个 32 位的无符号整型,占用 4 个字节(32位)。因此,无论使用 PhysicalAddress 还是 VirtualSize,联合体 Misc 在内存中占用的空间都是 4 个字节。

  • VirtualAddress: 节在虚拟地址空间中的起始地址。这是节在内存中加载时的起始位置。必须是SectionAlignment的整数倍。加上ImageBase,才是在内存中的实际地址。

    在 IMAGE_SECTION_HEADER 结构中,VirtualAddress 字段是一个非常重要的成员,它指定了节(section)在虚拟内存空间中的起始地址。这个地址是相对于可执行文件或 DLL 的基址(ImageBase)的相对虚拟地址(RVA)。当操作系统加载 PE(Portable Executable)文件到内存中时,它会根据 ImageBase 和 VirtualAddress 计算出节在物理内存中的实际地址(相加才是真实地址)。

    在 32 位 PE 文件中,VirtualAddress 是一个 DWORD 类型,表示一个 32 位的相对虚拟地址。在 64 位 PE 文件中,VirtualAddress 是一个 ULONGLONG 类型,表示一个 64 位的相对虚拟地址。

    VirtualAddress 字段的作用包括:

    1. 内存映射:操作系统使用 VirtualAddress 来确定节应该映射到进程的虚拟地址空间中的哪个位置。
    2. 地址转换:当程序运行时,CPU 会使用虚拟地址来访问内存中的数据。操作系统负责将虚拟地址转换为物理地址,这个过程称为地址转换。
    3. 节对齐:VirtualAddress 通常需要按照一定的对齐要求来设置,这是由 IMAGE_OPTIONAL_HEADER 中的 SectionAlignment 字段指定的。对齐可以提高内存访问的效率和性能。
    4. 重定位:在程序加载时,如果节中包含的地址需要根据实际的 ImageBase 进行调整(重定位),操作系统会使用 VirtualAddress 来执行这些调整。 例如,如果一个节的 VirtualAddress​ 被设置为 0x1000​,并且 ImageBase​ 被设置为 0x40000000​,那么该节在物理内存中的实际起始地址将是 0x40000000 + 0x1000​。

    请注意,VirtualAddress 只是一个起始点,节的实际大小由 SIZEOFRAWDATA 和 VirtualSize 字段确定。如果 SIZEOFRAWDATA 小于 VirtualSize,那么操作系统会在节的末尾添加填充(通常是零),以确保节的大小符合 VirtualSize 指定的大小。

  • SizeOfRawData: 节在文件中的原始数据大小(注意与节在内存中的大小进行区分,不一样)。这是实际存储在文件中的节的大小,可能小于 VirtualSize,因为可能会有填充。节在文件中按照文件对齐后的大小单位为字节,必须是FileAlignment的整数倍。

  • PointerToRawData: 指向节在文件中原始数据的指针。(在文件中的偏移)这是文件中节数据的起始位置。该值是FOA(File Offset Address)文件偏移地址,必须是FileAlignment 的整数倍。

    PointerToRawData 是 IMAGE_SECTION_HEADER 结构中的一个字段,它指定了节(section)的原始数据在 PE(Portable Executable)文件中的起始位置。这个字段表示从文件的开始到节的数据实际存放位置的偏移量。PointerToRawData 用于在文件中定位节的数据,以便在加载时可以正确地将数据读入内存。

    在 32 位 PE 文件中,PointerToRawData 是一个 DWORD 类型,表示一个 32 位的无符号整数值。在 64 位 PE 文件中,它通常是一个 ULONGLONG 类型,表示一个 64 位的无符号整数值。

    PointerToRawData 字段的作用包括:

    1. 数据定位:操作系统和加载器使用 PointerToRawData 来确定节的数据在文件中的确切位置,这对于读取文件到内存的映射至关重要。
    2. 文件 I/O:PointerToRawData 允许文件系统高效地进行输入输出操作,因为它直接指向节的数据,而不是整个文件的开始。
    3. 内存映射:在将文件映射到内存时,操作系统会使用 PointerToRawData 和 SizeOfRawData 来确定需要映射的文件部分。
    4. 节的连续性:PointerToRawData 有助于保持节数据在文件中的连续性,这对于优化文件访问和减少磁盘寻道时间很有帮助。 请注意,PointerToRawData​ 与 SizeOfRawData​ 字段一起使用,以确定节在文件中的范围。例如,如果 PointerToRawData​ 是 0x1000​,SizeOfRawData​ 是 0x2000​,那么节的数据将从文件的第 0x1000​ 字节开始,并且占用 0x2000​ 个字节的空间。操作系统在加载文件时会根据这些信息来读取和处理节的数据。
  • PointerToRelocations: 指向节的重定位表的指针。如果节包含需要重定位的地址,这个表将包含这些地址的信息。

  • PointerToLinenumbers: 指向节的行号表的指针。这个表包含了行号信息,通常用于调试目的。

  • NumberOfRelocations: 节中重定位项的数量。这表示节的重定位表中有多少个重定位项。

  • NumberOfLinenumbers: 节中行号项的数量。这表示节的行号表中有多少个行号项。

  • Characteristics: 节的特性和标志,这些标志定义了节的属性,如是否可读、可写、可执行等。

    Characteristics 字段在 IMAGE_SECTION_HEADER 结构中定义了 PE 节的特性和属性。以下是 Characteristics 字段可能包含的全部值,以及它们的解释和用途:

    以下是按值、代码和解释顺序列举的 IMAGE_SCN 宏定义,这些定义用于描述 PE 文件中节(section)的特性:

    1. 0x00000020 - IMAGE_SCN_CNT_CODE - 节包含代码。
       - 解释:此节包含可执行的代码。
    2. 0x00000040 - IMAGE_SCN_CNT_INITIALIZED_DATA - 节包含初始化数据。
       - 解释:此节包含已经初始化的数据。
    3. 0x00000080 - IMAGE_SCN_CNT_UNINITIALIZED_DATA - 节包含未初始化数据。
       - 解释:此节包含未初始化的数据,通常对应 .bss 段。
    4. 0x00000200 - IMAGE_SCN_LNK_INFO - 节包含注释或其他类型的信息。
       - 解释:此节可能包含开发者注释或其他类型的信息。
    5. 0x00000800 - IMAGE_SCN_LNK_REMOVE - 节内容不会成为映像的一部分。
       - 解释:此节在最终的可执行文件中将被移除,不会包含在最终的映像中。
    6. 0x00001000 - IMAGE_SCN_LNK_COMDAT - 节内容是 COMDAT。
       - 解释:此节可能在链接过程中与其他节合并,用于共享数据或代码。
    7. 0x00004000 - IMAGE_SCN_NO_DEFER_SPEC_EXC - 重置 TLB 条目中的推测异常处理位。
       - 解释:此标志用于处理器特定的节特性,确保 TLB 条目中的推测异常处理位被重置。
    8. 0x00008000 - IMAGE_SCN_GPREL - 节内容可以相对于 GP 访问。
       - 解释:此节的内容可以通过全局指针(GP)相对访问。
    9. 0x00008000 - IMAGE_SCN_MEM_FARDATA - 节内容是远数据。
       - 解释:此节用于存储远离 GP 的数据。
    10. 0x00020000 - IMAGE_SCN_MEM_PURGEABLE - 节是可清除的。
        - 解释:此节可以在内存紧张时被操作系统清除。
    11. 0x00020000 - IMAGE_SCN_MEM_16BIT - 节是 16 位的。
        - 解释:此节是为 16 位程序设计的。
    12. 0x00040000 - IMAGE_SCN_MEM_LOCKED - 节是锁定的。
        - 解释:此节一旦加载到内存中,将不会被交换出去。
    13. 0x00080000 - IMAGE_SCN_MEM_PRELOAD - 节是预加载的。
        - 解释:此节在程序启动时会被预加载到内存中。
    14. 0x01000000 - IMAGE_SCN_LNK_NRELOC_OVFL - 节包含扩展重定位。
        - 解释:此节包含超出标准重定位表的重定位信息。
    15. 0x02000000 - IMAGE_SCN_MEM_DISCARDABLE - 节可以被丢弃。
        - 解释:此节可以被操作系统丢弃,不包含重要数据。
    16. 0x04000000 - IMAGE_SCN_MEM_NOT_CACHED - 节不应被缓存。
        - 解释:此节不应该被缓存到处理器缓存中。
    17. 0x08000000 - IMAGE_SCN_MEM_NOT_PAGED - 节不应被分页。
        - 解释:此节不应该被交换到磁盘上。
    18. 0x10000000 - IMAGE_SCN_MEM_SHARED - 节是共享的。
        - 解释:此节可以被多个进程共享。
    19. 0x20000000 - IMAGE_SCN_MEM_EXECUTE - 节是可执行的。
        - 解释:此节包含可执行代码。
    20. 0x40000000 - IMAGE_SCN_MEM_READ - 节是可读的。
        - 解释:此节可以被读取。
    21. 0x80000000 - IMAGE_SCN_MEM_WRITE - 节是可写的。
        - 解释:此节可以被写入。
          请注意,某些值可能在不同的上下文中有不同的解释,或者可能在某些版本的 Windows SDK 中已不再使用。在使用这些宏定义时,应参考最新的官方文档和标准。

    请注意,对齐标志位(如 IMAGE_SCN_ALIGN_1BYTES 到 IMAGE_SCN_ALIGN_1024BYTES)是互斥的,只能设置其中一个。此外,Characteristics 字段的值通常是多个标志位的组合,这些标志位通过位运算(如按位或 |)来设置。例如,如果一个节既包含代码又需要按 4 字节对齐,那么 Characteristics 字段的值可能会设置为 IMAGE_SCN_CNT_CODE | IMAGE_SCN_ALIGN_4BYTES。

    这些特性标志对于操作系统、链接器和加载器来说非常重要,因为它们决定了如何处理节中的数据,以及如何将节映射到内存中。开发者和工具链在创建和处理 PE 文件时需要正确设置这些标志。

IMAGE_SECTION_HEADER 结构体为操作系统提供了加载和处理 PE 文件中各个节所需的信息。每个节都有一个 IMAGE_SECTION_HEADER 结构,这些结构通常在 PE 文件的 IMAGE_NT_HEADERS 部分后连续排列。

流程图总结

导出表

当 PE 文件被执行的时候,Windows 加载器将文件装入内存,并根据导入表 (Import Table) 登记的动态链接库(一般是 DLL 文件)一并装入地址空间,再根据 DLL 文件中的导出表 (Export Table) 获取的函数地址,对被执行文件的 IAT (导入地址表 Import Address Table) 进行填充和修正。

导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL 文件可以向系统提供导出函数的名称、序号和入口地址等信息,比便Windows 加载器通过这些信息来完成动态连接的整个过程。

注意:扩展名为.exe 的PE 文件中一般不存在导出表,而大部分的.dll 文件中都包含导出表。但注意,这并不是绝对的。例如纯粹用作资源的.dll 文件就不需要导出函数,另外有些特殊功能的.exe 文件也会存在导出函数。

导出表结构

导出表(Export Table)中的主要成分是一个表格,内含函数名称、输出序数等。序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。在此我们不提倡仅仅通过序数来索引函数的方法,这样会给DLL 文件的维护带来问题。例如当DLL 文件一旦升级或修改就可能导致调用该DLL 的程序无法加载到需要的函数。

数据目录表的第一个成员指向导出表,是一个IMAGE_EXPORT_DIRECTORY(以后简称IED)结构,IED 结构的定义如下:

IMAGE_EXPORT_DIRECTORY STRUCT【导出表,共40字节】
{
+00 h DWORD Characteristics ; 未使用,总是定义为0
+04 h DWORD TimeDateStamp ; 文件生成时间
+08 h WORD MajorVersion     ; 未使用,总是定义为0
+0A h WORD MinorVersion ; 未使用,总是定义为0
+0C h DWORD Name     ; 模块的真实名称
+10 h DWORD Base     ; 基数,加上序数就是函数地址数组的索引值
+14 h DWORD NumberOfFunctions ; 导出函数的总数
+18 h DWORD NumberOfNames ; 以名称方式导出的函数的总数
+1C h DWORD AddressOfFunctions ; 指向输出函数地址的RVA
+20 h DWORD AddressOfNames ; 指向输出函数名字的RVA
+24 h DWORD AddressOfNameOrdinals ; 指向输出函数序号的RVA
};IMAGE_EXPORT_DIRECTORY ENDS
//导出表总共四十字节
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        //未使用
    DWORD   TimeDateStamp;          //时间戳
    WORD    MajorVersion;           //未使用
    WORD    MinorVersion;           //未使用
    DWORD    Name ;                   //指向该导出表的文件名字符串RVA
    DWORD    Base ;                   //导出函数的起始序号(最后的起始序号-最初的起始序号+1)
    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. 从序号查找函数入口地址

大家来模拟一下Windows 装载器查找导出函数入口地址的整个过程。如果已知函数的导出序号,如何得到函数的入口地址呢 ?

Windows 装载器的工作步骤如下:

定位到PE 文件头

从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA

从导出表的 Base 字段(+0x10)得到起始序号

将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引

检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的

用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址

2. 从函数名称查找入口地址

如果已知函数的名称,如何得到函数的入口地址呢?与使用序号来获取入口地址相比,这个过程要相对复杂一点!

Windows 装载器的工作步骤如下:

最初的步骤是一样的,那就是首先得到导出表的地址

从导出表的 NumberOfNames 字段(+0x18)得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环

从 AddressOfNames 字段(+0x20)指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数

如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x

最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址

导出表RVA、内存对齐、文件对齐

TestDll的文件拖入winhex

此时导出表的RVA是018FA0,大小是181

内存对齐1000,文件对齐大小200

此时RVA ≠ FOA,需要进行转换

将TestDll拖入PETools,工具栏点击S出现区段表

Test DLL在PE工具中的区段展示

很明显在 .rdata段,计算文件中的偏移:

18FA0-17000=1FA0,1FA0+5A00=79A0

导出表字段详图

导入表

IMPORT_DESCRIPTOR结构体成员定义如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD    OriginalFirstThunk ;             //导入名称表(INT)的地址RVA
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                      //时间戳多数情况可忽略.如果是0xFFFFFFFF为绑定导入
    DWORD   ForwarderChain;                     //链表的前一个结构
    DWORD    Name ;                               //导入DLL文件名的地址RVA
    DWORD    FirstThunk ;                         //导入地址表(IAT)的地址RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
 
// 成员OriginalFirstThunk与FirstThunk都指向此结构 :
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;
 
// 如果结构IMAGE_THUNK_DATA32成员最高有效位(MSB)为1时低31位为导出序号.
//否则指向此结构. 
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;   //导出序号(有些编译器不会填充此值)
    CHAR   Name[1]; //该值长度不准确,以0结尾的字符串.导出函数名.
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

也就是说,DataDireactory的第二个元素是导入表的rva和大小,这个rva指向文件偏移0x22A10处,大小为0x78个字节,20个字节是一个导入表的结构,所以一共有5个导入表结构,因为最后一个是20个全0字节代表导入表结束。

下面主要研究导入表中的第一个元素,OriginalFirstThunk,

OriginalFirstThunk指向的是一个由IMAGE_THUNK_DATA结构体组成的数组,每个结构体的最高有效位为1,则低31位为函数导出序号;否则每个结构体指向一个由2字节Hint以及以00结尾的一个字符串组成的结构体IMAGE_IMPORT_BY_NAME。

第四个元素,Name,每个导入表的name元素是一个rva指向导入的dll字符串名称,在2进制中会以00结尾。

第五个元素,FirstThunk,FirstThunk指向的是一个由IMAGE_THUNK_DATA结构体组成的数组,在未执行前,也就是文件中时和OriginalFirstThunk指向的结构体完全一致。

OriginalFirstThunk和FirstThunk这两个成员解析

_IMAGE_IMPORT_DESCRIPTOR是PE文件格式中描述导入表的结构体,其中包含了两个成员:OriginalFirstThunk和FirstThunk。这两个成员都与导入表中的函数地址有关,但它们代表了不同的阶段和状态。

  1. OriginalFirstThunk:
    • OriginalFirstThunk是一个指向原始导入表(Original Import Table)的指针。在PE文件加载时,这个表包含了指向DLL导出表中函数名称的指针,这些指针指向了函数的名称,而不是函数的地址。
    • 这个成员用于在加载时解析函数的地址。加载器会使用这个表来查找DLL中实际的函数地址,并将这些地址填充到FirstThunk中。
    • 在加载过程中,OriginalFirstThunk通常不会被修改,它始终保持原始的导入表地址。
  2. FirstThunk:
    • FirstThunk是一个指向导入地址表(Import Address Table,简称IAT)的指针。这个表包含了实际的函数地址,即程序在运行时应该调用的函数的地址。
    • 一旦加载器解析了所有的导入函数地址,它就会更新FirstThunk指向的表,将名称指针替换为实际的函数地址。
    • 在程序运行期间,FirstThunk中的地址会被用来调用外部DLL中的函数。

总结来说,OriginalFirstThunk和FirstThunk的主要区别在于它们指向的内容和用途。OriginalFirstThunk指向原始的导入表,其中包含了函数名称的指针,用于加载时的地址解析。而FirstThunk指向解析后的导入地址表,其中包含了实际的函数地址,用于程序运行时的函数调用。两者共同协作,确保了程序能够正确地加载和使用外部DLL中的函数。

将petools拖入winhex中查看:

导入表在Directory中的位置

导入表字段详图

也就是说,DataDireactory的第二个元素是导入表的rva和大小,这个rva指向文件偏移0x22A10处,大小为0x78个字节,20个字节是一个导入表的结构,所以一共有5个导入表结构,因为最后一个是20个全0字节代表导入表结束。

下面主要研究导入表中的第一个元素,OriginalFirstThunk,

OriginalFirstThunk指向的是一个由IMAGE_THUNK_DATA结构体组成的数组,每个结构体的最高有效位为1,则低31位为函数导出序号;否则每个结构体指向一个由2字节Hint以及以00结尾的一个字符串组成的结构体IMAGE_IMPORT_BY_NAME。

第四个元素,Name,每个导入表的name元素是一个rva指向导入的dll字符串名称,在2进制中会以00结尾。

第五个元素,FirstThunk,FirstThunk指向的是一个由IMAGE_THUNK_DATA结构体组成的数组,在未执行前,也就是文件中时和OriginalFirstThunk指向的结构体完全一致。

重定位表

第一:重定位表是必要的,因为当程序被加载到内存时,它的基址(Base Address)是不确定的,可能会与编译时的预期地址不同。

第二:程序可能会使用多个动态链接库(DLLs),这些库在不同的情况下也可能被加载到不同的地址。因此,重定位表确保了程序中的函数调用和全局变量引用能够正确地指向目标地址。

第三:RVA = VA - IMAGEBASE

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;          //重定位数据所在页的rva
    DWORD   SizeOfBlock;             //当前页中重定位数据块的大小
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

整个减去前8个字节(rva和大小)除以2,就是需要修复的数据数量。

重定位数据块(Relocation Data Block)是 PE(Portable Executable)文件格式中用于在加载时修正程序内部地址的一种数据结构。它主要由以下几个部分组成:

  1. 重定位目录项(Relocation Directory Entry):

    这是数据目录表(Data Directory)中的一个条目,通常位于 PE 文件头的 OptionalHeader 部分。它指向重定位数据块的位置,并提供有关重定位信息的大小。IMAGE_DATA_DIRECTORY 结构体定义了重定位目录项,其中结构体的第六个成员也就是,包含两个成员:VirtualAddress 和 Size。VirtualAddress 指定了重定位数据块在 PE 文件中的相对虚拟地址(RVA),而 Size 指定了重定位数据块的大小。

  2. 重定位块头(Relocation Block Header):

    位于重定位数据块的开始部分,它包含两个字段:VirtualAddress 和 SizeOfBlock。VirtualAddress 指定了该重定位块所适用的节的 RVA,而 SizeOfBlock 指定了从当前位置开始的重定位条目块的大小。

  3. 重定位条目(Relocation Entries):

    紧随重定位块头之后的是一系列重定位条目。每个条目通常由一个字(WORD)组成,包含偏移量和重定位类型的信息。偏移量指示需要修正的地址相对于其所在节的基址的偏移。重定位类型指定了如何根据新基址修正偏移量,以及如何处理该地址。

  4. 重定位类型详细信息:

    重定位类型通常由重定位条目的高位字节指定,低位字节指定偏移量。重定位类型可以指示多种不同的修正操作,例如:

    • BASED_ABSOLUTE:不执行任何重定位,地址已经是绝对的。
    • BASED_HIGHLOW:将节的基址的高低位与偏移量组合起来形成完整的 32 位地址。
    • BASED_HIGHADJ:仅将节的基址的高位添加到偏移量中。
    • BASEDMachine_SPECIFIC:特定于目标机器的重定位类型。
  5. 目标地址信息:

    对于某些重定位类型,可能还需要额外的信息来确定最终的地址。例如,对于基于符号的重定位,可能需要引用导入表或导出表中的符号名称。

重定位数据块的目的是确保 PE 文件中的代码和数据在加载到任意内存地址时仍能正确地引用其他代码、数据和资源。在程序加载时,加载器会读取并解析这些重定位条目,并根据当前的内存布局对地址进行必要的修正。这是 Windows 操作系统支持动态链接和代码共享的一个重要机制。

如何修复重定位项

修复项的数量:(sizeofblock - 8) / 2 其实就是重定位条目的数量

VirtualAddress 4Byte 0x00011000
SizeOfBlock 4Byte 0x0000005C
0x3668 0x36B8
0x3708 0x3758
0x37BB 0x37C4
…
VirtualAddress 4Byte 0x00012000
SizeOfBlock 4Byte 0x000000A8
0x3008 0x3017
0x3035 0x316A
0x317D 0x3195
…
......
VirtualAddress 4Byte 0x00000000
SizeOfBlock 4Byte 0x00000000

高四位是3(0011)才需要修复,后12位为偏移,加上VirtualAddress 才是需要修复的地址。

从反汇编角度来理解修复重定位项

int main()
{
    dwData = 1;
//0411C11h  C7 05   3C A1 41 00   01 00 00 00      mov dword ptr[dwData(041A13Ch)], 1
//041A13Ch 这是变量的内存地址
//0400000h 这是当前工程的imagebase
//IMAGE BASE
//0500000h 如果工程此时的imagebase在0500000h,很明显不应该向041A13Ch中写入,而应该向051A13Ch中写入
//实际地址
//0511c11h C7 05 
//0511c13h  3C A1 51 00  01 00 00 00 所以要改变的其实就是要写入的地址
//被替换掉执行成功

    return 0;
 }
 从反汇编角度来理解修复重定位项

RVA(11c13h) = 411C13h(VA) - 0x40000(imagebase)

给出一个符合上述修复重定位的示例:

假设

重定位块VirtualAddress 11000h

重定位块 大小随便给一个20h

偏移3c13h,最高位是3,所以偏移是C13h

11000 + c13h = 11c13h

11c13 + 500000 = 511c13h 内存中编译的命令地址,原本该行命令从0411c11h这一行中的0411c13h位置的地址需要改变

41a13c - 400000 = 1a13c + 500000 = 51a13c 内存中的执行地址将变量存储在该地址

导入表偏移、内存大小、文件大小

将TestDll拖入PETools,工具栏点击S出现区段表

在内存中的rva是0x01F000,很明显是 .reloc段,在文件中是0x9200

重定位表字段详图

代码示例

打印 NT Header

头文件声明

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中
void MemToFile(char* szFilePath, char* Buffer, int FileSize);

tools.c源文件定义函数

//tools.c
#include <tools.h>

char* FileToMem(char* szFilePath, int* nFileSize)
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile) 
    {
        printf("FileToMem fopen fail\n");
        return;
    }
  
    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置
  
    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer) 
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);
  
    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);  //  fread 从文件中读取指定数量的块数据,并将数据存储在内存缓冲区中
  
    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n');
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }
  
    //判断PE标记
    DWORD;
    PDWORD;//相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);
  
    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n');
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }   
  
    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等
  
    fclose(pFile);
  
    *nFileSize = nSize;
  
    return pFileBuffer; //
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);
  
}

main函数打印

//main.c
//文件转内存,之后通过指针+结构体偏移输出pe标记
#include <tools.h>

int main()
{
    int FileSize = 0;//文件大小
    char* pFileBuffer = FileToMem(FILE_PATH_IN, &FileSize);//文件首地址
  
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;//强制转换   pimage_dos_header指向dos头的指针
   
   DWORD dwOffset = pDos->e_lfanew;//NT HEADER 距离程序首地址的偏移
   
   PIMAGE_DOS_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + dwOffest);
   
   printf("%08x \n", pNts->Signature);
   
    return 0;
}

打印 FILE Header

头文件声明

//tools.h
//在之前文件转内存,内存转文件功能之后加入打印fileheader的前两个字节内容(machine)
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中
void MemToFile(char* szFilePath, char* Buffer, int FileSize);

void printfileheader();

tools.c源文件定义函数

//tools.c
#include <tools.h>

char* FileToMem(char* szFilePath, int* nFileSize);
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile) 
    {
        printf("FileToMem fopen fail\n");
        return;
    }
  
    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置
  
    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer) 
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);
  
    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);
  
    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n');
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }
  
    //判断PE标记    DWORD   PDWORD 相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);
  
    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n');
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }   
  
    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等
  
    fclose(pFile);
  
    *nFileSize = nSize;
  
    return pFileBuffer; //
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);
  
}

void printfileheader()
{
    DWORD dwFileSize = 0;
  
    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*
  
    if (!pFileBuffer)
    {
        return;
    }
  
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
  
    printf("%d \n", pFil->Machine);
  
}
  
  

main函数打印

直接调用printfileheader()即可

打印 Section Header

头文件声明

//tools.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中

void MemToFile(char* szFilePath, char* Buffer, int FileSize);

void printfileheader();

void printfsectionheader();

tools.c源文件定义函数

//tools.c
#include <tools.h>

char* FileToMem(char* szFilePath, int* nFileSize)
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile)
    {
        printf("FileToMem fopen fail\n");
        return;
    }

    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置

    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer)
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);

    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);

    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n");
            fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    //判断PE标记    DWORD   PDWORD 相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);

    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n");
            fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等

    fclose(pFile);

    *nFileSize = nSize;

    return pFileBuffer; //
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);

}

void printfileheader()
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);

    printf("%d \n", pFil->Machine);

}

void printfsectionheader()
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR) + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    CHAR szName[9] = { 0 };
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(szName, psec[i].Name, IMAGE_SIZEOF_SHORT_NAME);
        printf("[%d] -> %s \n", i, szName);
        printf("[%d] -> %x \n", i, psec[i].Misc);
        printf("[%d] -> %x \n", i, psec[i].VirtualAddress);
        printf("[%d] -> %x \n", i, psec[i].SizeOfRawData);
        printf("[%d] -> %x \n", i, psec[i].Characteristics);

    }
}

main函数打印

//main.c
#include <tools.h>
#include <Windows.h>

int main()
{
    int FileSize = 0;//文件大小
    char* pFileBuffer = FileToMem(FILE_PATH_IN, &FileSize);//文件首地址

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;//强制转换   pimage_dos_header指向dos头的指针

    DWORD dwOffset = pDos->e_lfanew;//NT HEADER 距离程序首地址的偏移

    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + dwOffset);

    printf("%08x \n", pNts->Signature);

    return 0;
}

通过函数名Name打印导出表函数地址

头文件声明

//tools.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"        //地址要改
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"    //地址要改

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中

void MemToFile(char* szFilePath, char* Buffer, int FileSize);

void printfileheader();

void printfsectionheader();

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer);

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize);

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva);

tools.c源文件定义函数

//tools.c
#include "tools.h"

char* FileToMem(char* szFilePath, int* nFileSize)
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile)
    {
        printf("FileToMem fopen fail\n");
        return;
    }

    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置

    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer)
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);

    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);

    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    //判断PE标记    DWORD   PDWORD 相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);

    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等

    fclose(pFile);

    if (nFileSize != NULL)
    {
        *nFileSize = nSize;
    }


    return pFileBuffer; 
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);

}

void printfileheader() //输出标准PE头数据
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);

    printf("%d \n", pFil->Machine);

}

void printfsectionheader()
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)+IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    CHAR szName[9] = { 0 };
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(szName, psec[i].Name, IMAGE_SIZEOF_SHORT_NAME);
        printf("[%d] -> %s \n", i, szName);
        printf("[%d] -> %x \n", i, psec[i].Misc);
        printf("[%d] -> %x \n", i, psec[i].VirtualAddress);
        printf("[%d] -> %x \n", i, psec[i].SizeOfRawData);
        printf("[%d] -> %x \n", i, psec[i].Characteristics);

    }
}

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)  //文件缓存转换成内存缓存,内存缓存转换为文件缓存,保存文件。
{
    //定位pe结构
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pFileBuffer);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    //分配ImageBuffer对应的内存空间
    DWORD dwImageSize = pOpo->SizeOfImage;
    PCHAR pImageBuffer = (PCHAR)malloc(dwImageSize);
    if (!pImageBuffer)
    {
        return NULL;
    }
    memset(pImageBuffer, 0, dwImageSize);

    //拷贝pe头
    memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);

    //拷贝节区数据
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(pImageBuffer + psec[i].VirtualAddress,
            pFileBuffer + psec[i].PointerToRawData,
            psec[i].SizeOfRawData);
    }

    return pImageBuffer;
}

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize)
{
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pImageBuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pImageBuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //计算filebuffer大小 
    DWORD dwfilesize = popo->SizeOfHeaders;
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        dwfilesize += psec[i].SizeOfRawData;  // 头部的大小加上全部节区的大小,不能使用sizeofimage,这是内存中展开的大小
    }

    //分配内存
    PCHAR pfilebuffer = (PCHAR)malloc(dwfilesize);
    if (!pfilebuffer)
    {
        printf("malloc fail");
        return NULL;

    }

    //拷贝头加节表大小
    memcpy(pfilebuffer, pImageBuffer, popo->SizeOfHeaders);

    //循环拷贝节区数据
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        memcpy(
            pfilebuffer + psec[i].PointerToRawData,
            pImageBuffer + psec[i].VirtualAddress,
            psec[i].SizeOfRawData
        );
    }

    if (FileSize != NULL)
    {
        *FileSize = dwfilesize;
    }

    return pfilebuffer;
}

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva)
{
    //0x41a000
    //0x400000
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pbuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pbuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //判断是否在文件头内部
    if (dwrva < popo->SizeOfHeaders)
    {
        return dwrva;
    }


    //遍历节区查找匹配的rva
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        if ((dwrva >= psec[i].VirtualAddress) && ((dwrva <= psec[i].VirtualAddress + psec[i].Misc.VirtualSize)))
        {
            return dwrva - psec[i].VirtualAddress + psec[i].PointerToRawData;
        }
    }


    return 0;
}

main函数打印

//main.c
#include "tools.h"
#include <Windows.h>

PCHAR GetProcAddressByName(PCHAR pfilebuffer, PCHAR szfunname)
{
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pfilebuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pfilebuffer + pdos->e_lfanew);

    //判断pe文件是否有导出表
    if (pnth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
    {
        return;
    }

    DWORD dwexportfoa = rvatofoa(pfilebuffer, pnth->OptionalHeader.DataDirectory[0].VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY pexp = (PIMAGE_EXPORT_DIRECTORY)(pfilebuffer + dwexportfoa);

    //定位三张表
    PDWORD paddfun = (PDWORD)(pfilebuffer + rvatofoa(pfilebuffer, pexp->AddressOfFunctions));
    PDWORD paddnam = (PDWORD)(pfilebuffer + rvatofoa(pfilebuffer, pexp->AddressOfNames));
    PWORD paddord = (PWORD)(pfilebuffer + rvatofoa(pfilebuffer, pexp->AddressOfNameOrdinals));

    for (size_t i = 0; i < pexp->NumberOfNames; i++)
    {
        printf(" %s\n", pfilebuffer + rvatofoa(pfilebuffer, paddnam[i]));
        if (strcmp(szfunname, pfilebuffer + rvatofoa(pfilebuffer, paddnam[i])) == 0)
        {
            return paddfun[paddord[i]];
        }
    }  //循环打印函数名,匹配名称相符的函数返回该函数的rva

    return NULL;
}

int main()
{
    //读入DLL
    PCHAR pfilebuffer = FileToMem("C:\\Users\\13674\\Desktop\\TestDll\\Debug\\TestDll.dll", NULL);  // 需要改dll的文件地址 
    if (!pfilebuffer)
    {
        return;
    }

    PCHAR pfunaddr = GetProcAddressByName(pfilebuffer, "sub");
    printf("0x%08x", (DWORD)pfunaddr);  //输出sub函数的rva

    return 0;
}

通过序号Ordinal打印导出表函数地址

头文件声明

//tools.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"        //地址要改
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"    //地址要改

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中

void MemToFile(char* szFilePath, char* Buffer, int FileSize);

void printfileheader();

void printfsectionheader();

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer);

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize);

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva);

tools.c源文件定义函数

//tools.c
#include "tools.h"

char* FileToMem(char* szFilePath, int* nFileSize)
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile)
    {
        printf("FileToMem fopen fail\n");
        return;
    }

    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置

    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer)
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);

    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);

    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    //判断PE标记    DWORD   PDWORD 相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);

    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等

    fclose(pFile);

    if (nFileSize != NULL)
    {
        *nFileSize = nSize;
    }


    return pFileBuffer; 
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);

}

void printfileheader() //输出标准PE头数据
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);

    printf("%d \n", pFil->Machine);

}

void printfsectionheader()
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)+IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    CHAR szName[9] = { 0 };
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(szName, psec[i].Name, IMAGE_SIZEOF_SHORT_NAME);
        printf("[%d] -> %s \n", i, szName);
        printf("[%d] -> %x \n", i, psec[i].Misc);
        printf("[%d] -> %x \n", i, psec[i].VirtualAddress);
        printf("[%d] -> %x \n", i, psec[i].SizeOfRawData);
        printf("[%d] -> %x \n", i, psec[i].Characteristics);

    }
}

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)  //文件缓存转换成内存缓存,内存缓存转换为文件缓存,保存文件。
{
    //定位pe结构
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pFileBuffer);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    //分配ImageBuffer对应的内存空间
    DWORD dwImageSize = pOpo->SizeOfImage;
    PCHAR pImageBuffer = (PCHAR)malloc(dwImageSize);
    if (!pImageBuffer)
    {
        return NULL;
    }
    memset(pImageBuffer, 0, dwImageSize);

    //拷贝pe头
    memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);

    //拷贝节区数据
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(pImageBuffer + psec[i].VirtualAddress,
            pFileBuffer + psec[i].PointerToRawData,
            psec[i].SizeOfRawData);
    }

    return pImageBuffer;
}

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize)
{
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pImageBuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pImageBuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //计算filebuffer大小 
    DWORD dwfilesize = popo->SizeOfHeaders;
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        dwfilesize += psec[i].SizeOfRawData;  // 头部的大小加上全部节区的大小,不能使用sizeofimage,这是内存中展开的大小
    }

    //分配内存
    PCHAR pfilebuffer = (PCHAR)malloc(dwfilesize);
    if (!pfilebuffer)
    {
        printf("malloc fail");
        return NULL;

    }

    //拷贝头加节表大小
    memcpy(pfilebuffer, pImageBuffer, popo->SizeOfHeaders);

    //循环拷贝节区数据
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        memcpy(
            pfilebuffer + psec[i].PointerToRawData,
            pImageBuffer + psec[i].VirtualAddress,
            psec[i].SizeOfRawData
        );
    }

    if (FileSize != NULL)
    {
        *FileSize = dwfilesize;
    }

    return pfilebuffer;
}

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva)
{
    //0x41a000
    //0x400000
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pbuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pbuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //判断是否在文件头内部
    if (dwrva < popo->SizeOfHeaders)
    {
        return dwrva;
    }


    //遍历节区查找匹配的rva
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        if ((dwrva >= psec[i].VirtualAddress) && ((dwrva <= psec[i].VirtualAddress + psec[i].Misc.VirtualSize)))
        {
            return dwrva - psec[i].VirtualAddress + psec[i].PointerToRawData;
        }
    }


    return 0;
}

main函数打印

//main.c
#include <Windows.h>
#include "tools.h"

PCHAR GetProcAddressByOrid(PCHAR pfilebuffer, int dwordinal)
{
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)(pfilebuffer);
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pfilebuffer + pdos->e_lfanew);

    //判断pe文件是否有导出表
    if (pnth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
    {
        return;
    }

    DWORD dwexportfoa = rvatofoa(pfilebuffer, pnth->OptionalHeader.DataDirectory[0].VirtualAddress);
    PIMAGE_EXPORT_DIRECTORY pexp = (PIMAGE_EXPORT_DIRECTORY)(pfilebuffer + dwexportfoa);

    PDWORD paddfun = (PDWORD)(pfilebuffer + rvatofoa(pfilebuffer, pexp->AddressOfFunctions));

    return paddfun[dwordinal - pexp->Base];

}


int main()
{
    //读入DLL
    PCHAR pfilebuffer = FileToMem("C:\\Users\\13674\\Desktop\\TestDll\\Debug\\TestDll.dll", NULL);  //需要改dll的文件地址
    if (!pfilebuffer)
    {
        return;
    }

    PCHAR pfunaddr = GetProcAddressByOrid(pfilebuffer, 4);
    printf("0x%08x", (DWORD)pfunaddr);  //输出4号函数的rva

    return 0;
}

打印导入表

头文件声明

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN "C:\\Users\\13674\\Desktop\\PETool 1.0.0.5.exe"

PVOID FileToMem(IN PCHAR szFilePath, OUT LPDWORD dwFileSize);

VOID MemToFile(IN PCHAR szFilePath, IN PVOID pFileBuffer, IN DWORD dwFileSize);

VOID PrintImportInfo();

DWORD RvaToFoa(PCHAR pbuffer, DWORD dwrva); GetImportData.h

源文件定义函数

#include "GetImportData.h"

DWORD RvaToFoa(PCHAR pbuffer, DWORD dwrva)
{
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pbuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pbuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //判断是否在文件头内部
    if (dwrva < popo->SizeOfHeaders)
    {
        return dwrva;
    }


    //遍历节区查找匹配的rva
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        if ((dwrva >= psec[i].VirtualAddress) && ((dwrva <= psec[i].VirtualAddress + psec[i].Misc.VirtualSize)))
        {
            return dwrva - psec[i].VirtualAddress + psec[i].PointerToRawData;
        }
    }


    return 0;
}


PVOID FileToMem(IN PCHAR szFilePath, OUT LPDWORD dwFileSize)
{
    //打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile)
    {
        printf("FileToMem fopen Fail \r\n");
        return NULL;
    }

    //获取文件长度
    fseek(pFile, 0, SEEK_END);      //SEEK_END文件结尾
    DWORD Size = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);      //SEEK_SET文件开头

    //申请存储文件数据缓冲区
    PCHAR pFileBuffer = (PCHAR)malloc(Size);
    if (!pFileBuffer)
    {
        printf("FileToMem malloc Fail \r\n");
        fclose(pFile);
        return NULL;
    }

    //读取文件数据
    fread(pFileBuffer, Size, 1, pFile);

    //判断是否为可执行文件
    if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("Error: MZ \r\n");
        fclose(pFile);
        free(pFileBuffer);
        return NULL;
    }

    if (*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
    {
        printf("Error: PE \r\n");
        fclose(pFile);
        free(pFileBuffer);
        return NULL;
    }

    if (dwFileSize)
    {
        *dwFileSize = Size;
    }

    fclose(pFile);

    return pFileBuffer;
}

VOID MemToFile(IN PCHAR szFilePath, IN PVOID pFileBuffer, IN DWORD dwFileSize)
{
    //打开文件
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("MemToFile fopen Fail \r\n");
        return;
    }

    //输出文件
    fwrite(pFileBuffer, dwFileSize, 1, pFile);

    fclose(pFile);
}

VOID PrintImportInfo()
{
    //读取文件二进制数据
    DWORD dwFileSize = 0;
    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize);
    if (!pFileBuffer)
    {
        return;
    }

    //定位结构
    PIMAGE_DOS_HEADER        pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS        pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER     pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
    PIMAGE_OPTIONAL_HEADER32   pOpo = (PIMAGE_OPTIONAL_HEADER32)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);

    //判断该PE文件是否有重定位表
    if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)
    {
        printf("该PE文件不存在导入表 \r\n");
        return;
    }
    PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));

    //循环遍历
    while (pImp->OriginalFirstThunk && pImp->FirstThunk)
    {
        printf("-----------------------------------------------------------\r\n");
        PUCHAR pDllName = pFileBuffer + RvaToFoa(pFileBuffer, pImp->Name);
        printf("ModuleName -> [%s] \r\n", pDllName);


        //遍历INT
        printf("*****************INT*****************\r\n");
        PIMAGE_THUNK_DATA pInt = (PIMAGE_THUNK_DATA)(pFileBuffer + RvaToFoa(pFileBuffer, pImp->OriginalFirstThunk));
        do
        {
            //判断最高位
            if (*(PDWORD)pInt & IMAGE_ORDINAL_FLAG32)//IMAGE_ORDINAL_FLAG32 0x80000000
            {
                printf("ExportByOriginal -> [0x%08x] \r\n", *(PDWORD)pInt & 0x7FFFFFFF);
            }
            else
            {
                PIMAGE_IMPORT_BY_NAME pTemp = (PIMAGE_IMPORT_BY_NAME)(pFileBuffer + RvaToFoa(pFileBuffer, *(PDWORD)pInt));
                printf("ExportByName -> HINT[0x%04x] NAME[%s] \r\n", pTemp->Hint, pTemp->Name);
            }

        } while (*(PDWORD)(++pInt));


        //遍历IAT
        printf("*****************IAT*****************\r\n");
        PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(pFileBuffer + RvaToFoa(pFileBuffer, pImp->FirstThunk));

        //判断是否为绑定导入
        if (pImp->TimeDateStamp == 0)
        {
            do
            {
                //判断最高位
                if (*(PDWORD)pIat & IMAGE_ORDINAL_FLAG32)//IMAGE_ORDINAL_FLAG32 0x80000000
                {
                    printf("ExportByOriginal -> [0x%08x] \r\n", *(PDWORD)pIat & 0x7FFFFFFF);
                }
                else
                {
                    PIMAGE_IMPORT_BY_NAME pTemp = (PIMAGE_IMPORT_BY_NAME)(pFileBuffer + RvaToFoa(pFileBuffer, *(PDWORD)pIat));
                    printf("ExportByName -> HINT[0x%04x] NAME[%s] \r\n", pTemp->Hint, pTemp->Name);
                }

            } while (*(PDWORD)(++pIat));
        }
        else
        {
            do
            {
                printf("FunAddr -> [0x%08x] \r\n", *(PDWORD)pIat);

            } while (*(PDWORD)(++pIat));

        }

        //指向下一个导入表结构
        pImp++;
    }
}

 GetImportData.c

main函数输出

#include "GetImportData.h"

int main()
{
  PrintImportInfo();

  return 0;
} main.c

打印重定位表

所需头文件tools.h

给出所需函数声明

//tools.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>

#define FILE_PATH_IN   "D:\\PETool 1.0.0.5.exe"        //地址要改
#define FILE_PATH_OUT  "D:\\New_PETool 1.0.0.5.exe"    //地址要改

char* FileToMem(char* szFilePath, int* nFileSize); //选中函数名,按下alt + enter,就会创建函数定义在tools.c当中

void MemToFile(char* szFilePath, char* Buffer, int FileSize);

void printfileheader();

void printfsectionheader();

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer);

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize);

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva);

源文件tools.c

定义FileToMem函数,MemToFile函数,printfileheader函数,printfsectionheader函数,FileBufferToImageBuffer函数,ImageBufferToFileBuffer函数,rvatofoa函数。

//tools.c
#include "tools.h"

char* FileToMem(char* szFilePath, int* nFileSize)
{
    // 打开文件
    FILE* pFile = fopen(szFilePath, "rb");
    if (!pFile)
    {
        printf("FileToMem fopen fail\n");
        return;
    }

    // 获取文件长度
    fseek(pFile, 0, SEEK_END);
    int nSize = ftell(pFile);
    fseek(pFile, 0, SEEK_SET);  // 回到文件的开始位置

    // 申请内存   存储文件二进制数据
    char* pFileBuffer = (char*)malloc(nSize);
    if (!pFileBuffer)
    {
        printf("malloc fail\n");
        fclose(pFile);
        return;
    }memset(pFileBuffer, 0, nSize);

    // 读取数据
    fread(pFileBuffer, sizeof(char), nSize, pFile);

    //判断MZ标记
    if (*(short*)pFileBuffer != IMAGE_DOS_SIGNATURE)
    {
        printf("MZ \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    //判断PE标记    DWORD   PDWORD 相当于DWORD* 是一个指针类型
    DWORD dwOffest = *(PDWORD)(pFileBuffer + 0x3c);
    PDWORD PeSignature = (PDWORD)(pFileBuffer + dwOffest);

    if (*PeSignature != IMAGE_NT_SIGNATURE)  //if(*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3c)) != IMAGE_NT_SIGNATURE)这样写省去上面两步
    {
        printf("PE \n");
        fclose(pFile);
        free(pFileBuffer);
        pFileBuffer = NULL;
        return NULL;
    }

    // 这里可以对 pFileBuffer 进行其他操作,例如处理文件数据等

    fclose(pFile);

    if (nFileSize != NULL)
    {
        *nFileSize = nSize;
    }


    return pFileBuffer; 
}

void MemToFile(char* szFilePath, char* Buffer, int FileSize)
{
    FILE* pFile = fopen(szFilePath, "wb");
    if (!pFile)
    {
        printf("fopen fail \n");
        return;
    }
    fwrite(Buffer, FileSize, 1, pFile);
    fclose(pFile);

}

void printfileheader() //输出标准PE头数据
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);

    printf("%d \n", pFil->Machine);

}

void printfsectionheader()
{
    DWORD dwFileSize = 0;

    PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize); //char == CHAR    PCHAR == char*

    if (!pFileBuffer)
    {
        return;
    }

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)+IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    CHAR szName[9] = { 0 };
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(szName, psec[i].Name, IMAGE_SIZEOF_SHORT_NAME);
        printf("[%d] -> %s \n", i, szName);
        printf("[%d] -> %x \n", i, psec[i].Misc);
        printf("[%d] -> %x \n", i, psec[i].VirtualAddress);
        printf("[%d] -> %x \n", i, psec[i].SizeOfRawData);
        printf("[%d] -> %x \n", i, psec[i].Characteristics);

    }
}

PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)  //文件缓存转换成内存缓存,内存缓存转换为文件缓存,保存文件。
{
    //定位pe结构
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pFileBuffer);
    PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNts + 4);
    PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);

    //分配ImageBuffer对应的内存空间
    DWORD dwImageSize = pOpo->SizeOfImage;
    PCHAR pImageBuffer = (PCHAR)malloc(dwImageSize);
    if (!pImageBuffer)
    {
        return NULL;
    }
    memset(pImageBuffer, 0, dwImageSize);

    //拷贝pe头
    memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);

    //拷贝节区数据
    for (size_t i = 0; i < pFil->NumberOfSections; i++)
    {
        memcpy(pImageBuffer + psec[i].VirtualAddress,
            pFileBuffer + psec[i].PointerToRawData,
            psec[i].SizeOfRawData);
    }

    return pImageBuffer;
}

PCHAR ImageBufferToFileBuffer(PCHAR pImageBuffer, int* FileSize)
{
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pImageBuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pImageBuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //计算filebuffer大小 
    DWORD dwfilesize = popo->SizeOfHeaders;
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        dwfilesize += psec[i].SizeOfRawData;  // 头部的大小加上全部节区的大小,不能使用sizeofimage,这是内存中展开的大小
    }

    //分配内存
    PCHAR pfilebuffer = (PCHAR)malloc(dwfilesize);
    if (!pfilebuffer)
    {
        printf("malloc fail");
        return NULL;

    }

    //拷贝头加节表大小
    memcpy(pfilebuffer, pImageBuffer, popo->SizeOfHeaders);

    //循环拷贝节区数据
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        memcpy(
            pfilebuffer + psec[i].PointerToRawData,
            pImageBuffer + psec[i].VirtualAddress,
            psec[i].SizeOfRawData
        );
    }

    if (FileSize != NULL)
    {
        *FileSize = dwfilesize;
    }

    return pfilebuffer;
}

DWORD rvatofoa(PCHAR pbuffer, DWORD dwrva)
{
    //0x41a000
    //0x400000
    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)pbuffer;
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pbuffer + pdos->e_lfanew);
    PIMAGE_FILE_HEADER pfil = (PIMAGE_FILE_HEADER)((PCHAR)pnth + 4);
    PIMAGE_OPTIONAL_HEADER popo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pfil + IMAGE_SIZEOF_FILE_HEADER);
    PIMAGE_SECTION_HEADER psec = (PIMAGE_SECTION_HEADER)((PCHAR)popo + pfil->SizeOfOptionalHeader);

    //判断是否在文件头内部
    if (dwrva < popo->SizeOfHeaders)
    {
        return dwrva;
    }


    //遍历节区查找匹配的rva
    for (size_t i = 0; i < pfil->NumberOfSections; i++)
    {
        if ((dwrva >= psec[i].VirtualAddress) && ((dwrva <= psec[i].VirtualAddress + psec[i].Misc.VirtualSize)))
        {
            return dwrva - psec[i].VirtualAddress + psec[i].PointerToRawData;
        }
    }


    return 0;
}

打印重定位表中每一个重定位块

//main.c
#include "tools.h"
#include <Windows.h>

void PrintLocation()
{
    //读入文件
    PCHAR pfilebuffer = FileToMem("C:\\Users\\13674\\Desktop\\TestDll\\Debug\\TestDll.dll", NULL);//文件地址需更改
    if (!pfilebuffer)
    {
        return;
    }

    //定位结构
    PIMAGE_DOS_HEADER pdos = (PIMAGE_DOS_HEADER)(pfilebuffer);
    PIMAGE_NT_HEADERS pnth = (PIMAGE_NT_HEADERS)(pfilebuffer + pdos->e_lfanew);

    //判断是否存在重定位表
    if (pnth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress == NULL)
    {
        return;
    }

    DWORD dwfoarelocation = rvatofoa(pfilebuffer, pnth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    PIMAGE_BASE_RELOCATION prel = (PIMAGE_BASE_RELOCATION)(pfilebuffer + dwfoarelocation);

    while (*(PLONGLONG)prel)
    {
        printf("virtualaddress -> [0x%08X] \n", prel->VirtualAddress);
        printf("sizeofblock    -> [0x%08X] \n", prel->SizeOfBlock);

        DWORD dwrelentry = (prel->SizeOfBlock - 8) / 2;
        PWORD preldata = (PWORD)((PCHAR)prel + 8);

        for (size_t i = 0; i < dwrelentry; i++)
        {
            if ((preldata[i] & 0x3000) == 0x3000)
            {
                DWORD dwoffest = preldata[i] & 0xFFF;
                DWORD dwrva = dwoffest + prel->VirtualAddress;

                printf("%x \n", dwrva);
            }
        }

        prel = (PIMAGE_BASE_RELOCATION)((PCHAR)prel + prel->SizeOfBlock);
    }
  


}

int main()
{
    PrintLocation();

    return 0;
}

参考文献

  1. CSDN. PE文件之导出表(IMAGE_EXPORT_DIRECTORY)

文章作者: 曙光
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 曙光 !
赏
  目录