郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

前言

不管你是逆向领域,还是开发领域,异或是病毒编写者(红蓝对抗),都需要对PE文件有详细的了解,由于自己是个菜逼然后需要用到修改PE结构来达到某些免杀的操作,既然这样我就祭出菜逼大杀器Google百度来自行学习,本篇文章主要都是摘抄各位师傅们的文章,加上菜逼我的理解混合在一起的,如果有哪些地方理解错了或者填错了请各位师傅斧正!!

img

总体介绍

可执行文件(Executable File)是指可以由操作系统直接加载执行的文件,在Windows操作系统中可执行文件就是PE文件结构,在Linux下则是ELF文件,下面这张图就是PE文件格式的图片(来自看雪),非常大一张图片,其实PE格式就是各种结构体的结合,Windows下PE文件的各种结构体在WinNT.h这个头文件中,可以在VS(宇宙无敌第一编译器)中查询。

img

PE文件整体结构

PE结构可以大致分为:

  • DOS部分
  • PE文件头
  • 节表(块表)
  • 节数据(块数据)
  • 调试信息

依旧用看雪的图来,看雪NB~奥利给!

img

PE指纹(DOS头)

我们需要清楚的概念是PE指纹,也就是判断一个文件是否是PE文件的依据,首先是根据文件的前两个字节是否为4D 5A,也就是MZ,接着第四行的最后4个字节表示的是NT头的起始位置,我们使用32位程序作为分析

DOS头里面分为(分HeaderDOS存根)

Header结构(00000000 - 0000003F,共64个字节)

2

typedef struct _IMAGE_DOS_HEADER { 
USHORT e_magic; // DOS签名“MZ-->Mark Zbikowski(设计了DOS的工程师)” -> 4D 5A
USHORT e_cblp; // 文件最后页的字节数 -> 00 90 -> 144
USHORT e_cp; // 文件页数 -> 00 30 -> 48
USHORT e_crlc; // 重定义元素个数 -> 00 00
USHORT e_cparhdr; // 头部尺寸,以段落为单位 -> 00 04
USHORT e_minalloc; // 所需的最小附加段 -> 00 00
USHORT e_maxalloc; // 所需的最大附加段 -> FF FF
USHORT e_ss; // 初始的SS值(相对偏移量) -> 00 00
USHORT e_sp; // 初始的SP值 -> 00 B8 -> 184
USHORT e_csum; // 校验和 -> 00 00
USHORT e_ip; // 初始的IP值 -> 00 00
USHORT e_cs; // 初始的CS值(相对偏移量) -> 00 00
USHORT e_lfarlc; // 重分配表文件地址 -> 00 40 -> 64
USHORT e_ovno; // 覆盖号 -> 00 00
USHORT e_res[4]; // 保留字 -> 00 00 00 00 00 00 00 00
USHORT e_oemid; // OEM标识符(相对e_oeminfo) -> 00 00
USHORT e_oeminfo; // OEM信息 -> 00 00
USHORT e_res2[10]; // 保留字 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LONG e_lfanew; // 指示NT头的偏移,也是DOS存根的结束位置(根据不同文件拥有可变值) -> 00 00 01 08 -> 264
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

注意Win+Intel的电脑上大部分采用”小端法”,字节在内存中储存方式是倒过来的。

重要参数为e_magice_lfanew,且e_lfanew的值在文件偏移量处0x3c的位置,大小4字节

前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究

DOS存根(00000040 - X)

存根的大小是变的,起始位置为40h,结束位置由30h中最后面4个字节决定的

2

DOS存根则是一段简单的DOS程序,主要用来输出类似This program cannot be run in DOS mode.的提示语句。即使没有DOS存根,程序也能正常执行。

NT头(PE头)

PE最重要的头,长度由DOS头 e_lfanew 决定

IMAGE_NT_HEADERS

从PE字符串开始到.text字符串结束,如下图画出的,大小为f8h

2

// 32位程序的NT_HEADERS
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // [0x00] PE标识 // 0x4550
IMAGE_FILE_HEADER FileHeader; // [0x04] 文件基本信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // [0x18] 文件扩展信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
// 64位程序的NT_HEADERS64
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; // [0x00] PE标识 // 0x4550
IMAGE_FILE_HEADER FileHeader; // [0x04] 文件基本信息
IMAGE_OPTIONAL_HEADER64 OptionalHeader; // [0x18] 文件扩展信息
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

然后里面分别有3个字段分别是SignatureIMAGE_FILE_HEADER结构体IMAGE_OPTIONAL_HEADER结构体大小分别是04h14he0h

Signatur

也称作PE签名,这个成员和DOS头的MZ标记一样都是一个PE文件的标准特征,如果把这个PE签名修改后,程序也是不会正常运行的(跳出黑窗口打印This program cannot be run in DOS mode然后闪退,可能是因为修改PE签名后无法识别后续内容的关系吧)。

IMAGE_FILE_HEADER

其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。

2

typedef struct _IMAGE_FILE_HEADER { 
WORD Machine; // Intel 386以及后续 -> 01 4c -> 兼容32位Intel X86芯片
// x64:8664 可以运行在什么平台上 任意:0
WORD NumberOfSections; // 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 07 -> 7
DWORD TimeDateStamp; // PE文件的创建时间,一般有连接器填写 -> 5D 37 24 CC ->1563894988
DWORD PointerToSymbolTable; // COFF文件符号表在文件中的偏移 -> 00 00 00 00
DWORD NumberOfSymbols; // 符号表的数量 -> 00 00 00 00
WORD SizeOfOptionalHeader; // 指出IMAGE_OPTIONAL_HEADER32结构体的长度。-> 00 E0 -> 224字节
// PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
// 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。
WORD Characteristics; // 标识文件的属性,二进制中每一位代表不同属性 -> 01 02
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

第1部分(20个字节)

偏移 大小 英文名 中文名 描述
0 2 Machine 机器数 标识CPU的数字。
2 2 NumberOfSections 节数 它的含义就是当前PE文件的节区数量,虽然它是大小是两个字节,但是在windows加载程序时会将节区的最大数量限制为96个。
4 4 TimeDateStamp 时间/日期标记 UTC时间1970年1月1日00:00起的总秒数的低32位,它指出文件何时被创建。
8 8 已经废除
16 2 SizeOfOptionalHeader 可选头大小 第2部分+第3部分的总大小。这个大小在32位和64位文件中是不同的。对于32位文件来说,它是224;对于64位文件来说,它是240。
18 2 Characteristics 文件特征值 指示文件属性的标志。参考特征
  • Machine

    所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。镜像文件仅能运行于指定处理器或者能够模拟指定处理器的系统上。

    具体机器类型如下图

    描述
    0x0 适用于任何类型处理器
    0x1d3 Matsushita AM33处理器
    0x8664 x64处理器
    0x1c0 ARM小尾处理器
    0xebc EFI字节码处理器
    0x14c Intel 386或后继处理器及其兼容处理器
    0x200 Intel Itanium处理器
    0x9041 Mitsubishi M32R小尾处理器
    0x266 MIPS16处理器
    0x366 带FPU的MIPS处理器
    0x466 带FPU的MIPS16处理器
    0x1f0 PowerPC小尾处理器
    0x1f1 带符点运算支持的PowerPC处理器
    0x166 MIPS小尾处理器
    0x1a2 Hitachi SH3处理器
    0x1a3 Hitachi SH3 DSP处理器
    0x1a6 Hitachi SH4处理器
    0x1a6 Hitachi SH5处理器
    0x1c2 Thumb处理器
    0x169 MIPS小尾WCE v2处理器
  • NumberOfSections

    010 Editor模版上验证节区数量

    image-20200421231446813

  • TimeDateStamp

    它的含义是时间戳,用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00起开始计数的,计数单位是秒

  • SizeOfOptionalHeader
    它存储该PE文件的可选PE头的大小,在32位PE文件中可选头大小为0xE0,64位可选头大小为0xF0。正因为如此,所以就必须通过该成员来确定可选PE头的大小。

  • Characteristics

    域包含镜像文件属性的标志。以下加粗的是常用的属性。当前定义了以下值(由低位往高位)

    位置 描述
    0 它表明此文件不包含基址重定位信息,因此必须被加载到其首选基地址上。如果基地址不可用,加载器会报错。
    1 它表明此镜像文件是合法的。看起来有点多此一举,但又不能少。
    2 保留,必须为0。
    3
    4
    5 应用程序可以处理大于2GB的地址。
    6 保留,必须为0。
    7
    8 机器类型基于32位体系结构。
    9 调试信息已经从此镜像文件中移除。
    10 如果此镜像文件在可移动介质上,完全加载它并把它复制到交换文件中。几乎不用
    11 如果此镜像文件在网络介质上,完全加载它并把它复制到交换文件中。几乎不用
    12 此镜像文件是系统文件,而不是用户程序。
    13 此镜像文件是动态链接库(DLL)。
    14 此文件只能运行于单处理器机器上。
    15 保留,必须为0。

IMAGE_OPTIONAL_HEADER

其中有9个重要参数,设置错误会导致文件无法运行

2

typedef struct _IMAGE_OPTIONAL_HEADER { 
WORD Magic; // 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107
BYTE MajorLinkerVersion; // 链接器的主版本号 -> 0C
BYTE MinorLinkerVersion; // 链接器的次版本号 -> 00
DWORD SizeOfCode; // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 00 41 56 00
DWORD SizeOfInitializedData; // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 00 0B A2 00
DWORD SizeOfUninitializedData; // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
DWORD AddressOfEntryPoint; // 指出程序最先执行的代码起始地址(RVA) -> 00 1F A9 D3
DWORD BaseOfCode; // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 00 00 10 00
DWORD BaseOfData; // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 00 00 10 00
// 在64位文件中此处被并入紧随其后的ImageBase中。
DWORD ImageBase; // 当加载进内存时,镜像的第1个字节的首选地址。
// WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
// 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
// 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。
DWORD SectionAlignment; // SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00
DWORD FileAlignment; // FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
// SectionAlignment必须大于或者等于FileAlignment

WORD MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 06
WORD MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
WORD MajorImageVersion; // 镜像的主版本号 -> 00 00
WORD MinorImageVersion; // 镜像的次版本号 -> 00 00
WORD MajorSubsystemVersion; // 子系统的主版本号 -> 00 06
WORD MinorSubsystemVersion; // 子系统的次版本号 -> 00 00
DWORD Win32VersionValue; // 保留,必须为0 -> 00 00 00 00
DWORD SizeOfImage; // 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
// 一般文件大小与加载到内存中的大小是不同的。 -> 00 6C 80 00
DWORD SizeOfHeaders; // 所有头的总大小,向上舍入为FileAlignment的倍数。
// 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00
DWORD CheckSum; // 镜像文件的校验和 -> 00 00 00 00
WORD Subsystem; // 运行此镜像所需的子系统 -> 00 03 -> Windows字符模式(CUI)子系统(从命令提示符启动的)具体可以看下面的Windows子系统表格
// 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll)
WORD DllCharacteristics; // DLL标识 -> 81 40 可以参考下面表格DLL特征
DWORD SizeOfStackReserve; // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
DWORD SizeOfStackCommit; // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
DWORD SizeOfHeapReserve; // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
DWORD SizeOfHeapCommit; // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
DWORD LoaderFlags; // 保留,必须为0 -> 00 00 00 00
DWORD NumberOfRvaAndSizes; // 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

还需要知道的是,程序的真正入口点 = ImageBase + AddressOfEntryPoint

  • Magic
    这个无符号整数指出了镜像文件的状态,此成员可以是以下的值

    宏定义 数值 描述
    IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x010B 表明这是一个32位镜像文件。
    IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x020B 表明这是一个64位镜像文件。
    IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x0107 表明这是一个ROM镜像。
  • AddressOfEntryPoint
      该成员保存着文件被执行时的入口地址,它是一个RVA。如果想要在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址。

  • ImageBase
    该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。当文件被载入其他地址后,就必须通过重定位表进行资源的重定位,这就会变慢文件的载入速度。而装载到ImageBase指定的地址就不会进行资源重定位。

  • SectionAlignment
    该成员指定了文件被装入内存时,节区的对齐单位。节区被装入内存的虚拟地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该成员的默认大小为系统的页面大小。

  • FileAlignment
    该成员指定了文件在硬盘上时,节区的对齐单位。节区在硬盘上的地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该值应为200h到10000h(含)之间的2的幂。默认为200h。如果SectionAlignment的值小于系统页面大小,则FileAlignment的值必须等于SectionAlignment的值。

  • SizeOfImage
    该成员指定了文件载入内存后的总体大小,包含所有的头部信息。并且它的值必须是SectionAlignment的整数倍。

  • Subsystem

    定义了以下值以确定运行镜像所需的Windows子系统(如果存在)

    描述
    0 未知子系统
    1 设备驱动程序和Native Windows进程
    2 Windows图形用户界面(GUI)子系统(一般程序)
    3 Windows字符模式(CUI)子系统(从命令提示符启动的)
    7 Posix字符模式子系统
    9 Windows CE
    10 可扩展固件接口(EFI)应用程序
    11 带引导服务的EFI驱动程序
    12 带运行时服务的EFI驱动程序
    13 EFI ROM镜像
    14 XBOX
  • DllCharacteristics域定义了以下值

    位置 描述
    1 保留,必须为0。
    2
    3
    4
    5 官方文档缺失
    6 官方文档缺失
    7 DLL可以在加载时被重定位。
    8 强制进行代码完整性校验。
    9 镜像兼容于NX。
    10 可以隔离,但并不隔离此镜像。
    11 不使用结构化异常(SE)处理。
    12 不绑定镜像。
    13 保留,必须为0。
    14 WDM驱动程序。
    15 官方文档缺失
    16 可以用于终端服务器。
IMAGE_DATA_DIRECTORY

数据目录结构,里面列举了导入表、导出表、重定位表等一系列表

typedef struct _IMAGE_DATA_DIRECTORY {  
DWORD VirtualAddress; //指向某个数据的相对虚拟地址
DWORD Size; //某个数据块的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

010 Editor中打开的目录如下,里面每个表都是一个完整的IMAGE_DATA_DIRECTORY结构体image-20200422000024112

节表

节表的结构如下

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // ASCII字符串 可自定义 只截取8个字节
union { // 该节在没有对齐之前的真实尺寸,该值可以不准确
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // 节大小
DWORD VirtualAddress; // 内存中的偏移地址
DWORD SizeOfRawData; // 节在文件中对齐的尺寸
DWORD PointerToRawData; // 节区在文件中的偏移
DWORD PointerToRelocations; // 重定位的偏移(用于OBJ文件)
DWORD PointerToLinenumbers; // 行号表的偏移(用于调试)
WORD NumberOfRelocations; // 重定位表的数量(用于OBJ文件)
WORD NumberOfLinenumbers; // 行号表数量(用于调试)
DWORD Characteristics; // 节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

kernel23.dll举例,里面有6个节表,每个节表代表一个IMAGE_SECTION_HEADER结构体image-20200421235002248

节表格式如下

偏移 大小 英文名 描述
0 8 Name 这是一个8字节ASCII编码的字符串,不足8字节时用NULL填充,必须使其达到8字节。如果它正好是8字节,那就没有最后的NULL字符。可执行镜像不支持长度超过8字节的节名。
8 4 VirtualSize 当加载进内存时这个节的大小。如果此值比SizeOfRawData大,那么多出的部分用0填充。这是节的数据在没有进行对齐处理前的实际大小,不需要内存对齐。
12 4 VirtualAddress 内存中节相对于镜像基址的偏移。必须是SectionAlignment的整数倍。
16 4 SizeOfRawData 磁盘文件中初始化数据的大小。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。
20 4 PointerToRawData 节中数据起始的文件偏移。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。
24 4 PointerToRelocations 重定位项开头的文件指针。对于可执行文件或没有重定位项的文件来说,此值应该为0。
28 4 已经废除。
32 2 NumberOfRelocations 节中重定位项的个数。对于可执行文件或没有重定位项的文件来说,此值应该为0。
34 2 已经废除。
36 4 Characteristics 描述节特征的标志。参考节标志。

单独一个节表的详细结构如下图

image-20200421235345057

  • Name
    这是一个8字节的ASCII字符串,长度不足8字节时用0x00填充可执行文件不支持长度超过8字节的节名。对于支持超过字节长度的文件来说,此成员会包含斜杠(/),并在后面跟随一个用ASCII表示的十进制数字,该数字是字符串表的偏移量。

  • VirtualAddress
      指定了该节区装入内存虚拟空间后的地址,这个地址是一个相对虚拟地址(RVA),它的值一般是SectionAlignment的整数倍。它加上ImageBase后才是真正的虚拟地址。

  • SizeOfRawData
      指定了该节区在硬盘上初始化数据的大小,以字节为单位。它的值必须是FileAlignment的整数倍,如果小于Misc.VirtualSize,那么该部分的其余部分将用0x00填充。如果该部分仅包含未初始化的数据,那么这个值将会为零。

  • PointerToRawData
    指出零该节区在硬盘文件中的地址,这个数值是从文件头开始算起的偏移量,也就是说这个地址是一个文件偏移地址(FOA)。它的值必须是FileAlignment的整数倍。如果这个部分仅包含未初始化的数据,则将此成员设置为零。

  • Characteristics
    该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,比较常见的定义如下,更多信息见 winnt.h 的定义

    #define IMAGE_SCN_CNT_CODE               0x00000020  // 此节包含代码
    #define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 此节包含已初始化数据
    #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 此节包含未初始化数据
    #define IMAGE_SCN_LNK_INFO 0x00000200 // 此节包含注释或其他类型信息
    #define IMAGE_SCN_LNK_REMOVE 0x00000800 // 此节不会成为映像的一部分
    #define IMAGE_SCN_LNK_COMDAT 0x00001000 // 此节包含COM数据
    #define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // 在此节的TLB项中重置异常控制位
    #define IMAGE_SCN_GPREL 0x00008000 // 此节可以访问GP相关内容
    #define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // 此节包含扩展重定位信息
    #define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 此节可以被丢弃(比如重定位.reloc节)
    #define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // 此节不可以缓存
    #define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // 此节不可以分页
    #define IMAGE_SCN_MEM_SHARED 0x10000000 // 此节是共享的
    #define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 此节是可执行的
    #define IMAGE_SCN_MEM_READ 0x40000000 // 此节是可读的
    #define IMAGE_SCN_MEM_WRITE 0x80000000 // 此节是可写的

虽然每个节的名称可以随意定义,但是也有一些约定俗成的名称,如下

名称 描述
.text 代码段,Borland C++编译器代码段为code
.data 可读写的数据段,存放全局变量或静态变量
.rdata 只读数据段,存放常量信息
.idata 导入数据段,存放导入表信息
.edata 导出数据段,存放导出表信息
.rsrc 资源段,存放图标、菜单等资源信息
.bss 未初始化数据段
.crt 存放用于支持C++运行时库(CRT)所添加的数据
.tls 存放用于支持通过_declspec(thread)声明的线程局部存储数据
.reloc 存放重定位信息
.sdata 存放可被全局指针定位的可读写数据
.srdata 存放可被全局指针定位的只读数据
.pdata 存放异常表,结构体为IMAGE_RUNTIME_FUNTCION_ENTRY
.debug$S 存放OBJ文件中Codeview格式符号
.debug$T 存放OBJ文件中Codeview格式类型符号
.debug$P 存放使用预编译头时的一些信息
.drectve 存放编译时的一些链接命令
.didat 存放延迟装入的数据

导入表

导出表(Import Table),主要就是标注了需要导入使用的一些DLL文件

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA 指向 INT (PIMAGE_THUNK_DATA结构数组)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 时间戳多数情况可忽略 如果是0xFFFFFFFF表示IAT表被绑定为函数地址
DWORD ForwarderChain; // 转发链,如果不转发则为0
DWORD Name; // 导入DLL文件名的RVA地址
DWORD FirstThunk; // 导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

如下图可知道,需要用到的dll文件

image-20200422001214627

EXE文件载入后对应的导入表结构图

载入后导入表结构图

EXE文件载入前对应的导入表结构图

载入前导入表结构图

导出表

导出表(Export Table)一般是DLL文件用的比较多,exe文件很少有导出表,导出表的数据结构如下

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 保留,恒为0x00000000
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 主版本号,一般不赋值
WORD MinorVersion; // 子版本号,一般不赋值
DWORD Name; // 指针指向该导出表文件名字符串
DWORD Base; // 导出函数起始序号,索引基数
DWORD NumberOfFunctions; // 导出地址表中的成员个数
DWORD NumberOfNames; // 导出名称表中的成员个数
DWORD AddressOfFunctions; // 指针指向导出函数地址表(EAT)
DWORD AddressOfNames; // 指针指向导出函数名称表(ENT)
DWORD AddressOfNameOrdinals; // 指针指向导出函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • AddressOfFunctions
    这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。
  • AddressOfNames
    这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。
  • AddressOfNameOrdinals
      这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
  • NumberOfFunctions
    注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。

image-20200422000611876

通过函数名查找函数地址

按函数名查找函数地址

通过函数序号查找函数地址

通过函数序号查找函数地址

重定位表

系统在加载DLL文件时,并不是每次都能加载到预期的 ImageBase 基址上,所以DLL都存在 基址重定位表
用来修正相关的地址信息,另外EXE的 动态基址 技术,也是用 基址重定位表 实现的。

PE文件中的重定位信息是由多个 IMAGE_BASE_RELOCATION 结构体组成的,每个结构体只描述一个 4KB 大小
的分页内重定位信息,也就是 0x1000 字节,因此结构体中 VirtualAddress 的值总是为 0x1000 的倍数。

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 需要重定位数据的起始RVA
DWORD SizeOfBlock; // 本结构与TypeOffset总大小
//WORD TypeOffset[1]; // 原则上不属于本结构
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

重定位的本质非常简单,就是比较实际加载地址与 ImageBase 的值,如果相等则不需要做任何操作,如果
不相等就需要把重定位表中指定的地址处加上这个差值。

TypeOffset 由两部分数据组成,高4位 表示 类型低12位 表示 偏移。类型定义如下

信息 宏定义
0 无重定位操作,填0后用于4字节对齐 IMAGE_REL_BASED_ABSOLUTE
1 重定位偏移指向位置的高2个字节需要被修正 IMAGE_REL_BASED_HIGH
2 重定位偏移指向位置的高2个字节需要被修正 IMAGE_REL_BASED_LOW
3 重定位偏移指向的4个字节的地址需要被修正 IMAGE_REL_BASED_HIGHLOW
4 需要使用两项TypeOffset才能完成索引操作 IMAGE_REL_BASED_HIGHADJ
5 基址重定位应用于MIPS jump指令 IMAGE_REL_BASED_MIPS_JMPADDR
6 保留 IMAGE_REL_BASED_RESERVED
9 基址重定位应用于MIPS16 jump指令 IMAGE_REL_BASED_IA64_IMM64
10 重定位偏移指向的8个字节(64位)地址需要被修正 IMAGE_REL_BASED_DIR64

重定位表结构如下图所示

结构图

资源表

资源表用来存储程序的各种界面数据,比如菜单、图标、版本信息等,其结构体如下

typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; // 资源属性标识,通常为0x00000000
DWORD TimeDateStamp; // 资源建立的时间
WORD MajorVersion; // 资源主版本,通常为0x0004
WORD MinorVersion; // 资源子版本,通常为0x0000
WORD NumberOfNamedEntries; // 资源名称条目个数
WORD NumberOfIdEntries; // 资源ID条目个数
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

资源在PE文件中是以目录结构的形式存在的,一般情况下这个目录分3层,从根目录开始分别为资源类型、
目录资源ID、资源代码页。每层的头部是一个 IMAGE_RESOURCE_DIRECTORY 结构,并且在其后面跟着一个
IMAGE_RESOURCE_DIRECTORY_ENTRY 结构数组,然后结构数组的每个成员则分别指向下一层目录结构。

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset : 31; // 资源名偏移
DWORD NameIsString : 1; // 资源名为字符串
};
DWORD Name; // 资源/语言类型
WORD Id; // 资源数字ID
};
union {
DWORD OffsetToData; // 数据偏移地址
struct {
DWORD OffsetToDirectory : 31; // 子目录偏移地址
DWORD DataIsDirectory : 1; // 数据为目录
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

第一个联合体的信息,是根据当前结构体所处的目录层次来决定的,位于 第1层 目录时 Name 有效,保存的
信息是 资源类型。位于 第2层 目录时 Id结构体 有效,取决于此资源的 索引方式,如果用的是 编号索引
就是 Id 有效,否则 结构体 有效。位于 第3层 目录时 Name 有效,保存的信息是 语言类型

第二个联合体的信息,理论上是根据具体情况而定的,如果下级是一个 子目录 的话,那么就是 结构体 生效,
如果下级是 资源数据 则是字段 OffsetToData 生效。

NameIsString1 时,NameOffset 指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构体

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; // 字符串的字节数
WCHAR NameString[1]; // 字符串的内容信息
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

Name 位于 第1层 目录时表示的 资源类型 如下表所示

类型值 资源类型 类型值 资源类型
0x00000001 鼠标指针(Cursor) 0x00000008 字体(Font)
0x00000002 位图(Bitmap) 0x00000009 快捷键(Accelerators)
0x00000003 图标(Icon) 0x0000000A 非格式化资源(Unformatted)
0x00000004 菜单(Menu) 0x0000000B 消息列表(Message Table)
0x00000005 对话框(Dialog) 0x0000000C 鼠标指针组(Group Cursor)
0x00000006 字符串列表(String Table) 0x0000000E 图标组(Group Icon)
0x00000007 字体目录(Font Directory) 0x00000010 版本信息(Version Information)

在经过3层目录的索引后,最后是一个 IMAGE_RESOURCE_DATA_ENTRY 结构体,定义如下

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // 资源数据的RVA
DWORD Size; // 资源数据的大小
DWORD CodePage; // 代码页
DWORD Reserved; // 保留字段,通常为0x00000000
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

资源表的3层目录关系,如下图所示

关系图

参考文章

https://thunderjie.github.io/2019/03/27/PE%E7%BB%93%E6%9E%84%E8%AF%A6%E8%A7%A3/
https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.1
https://www.jianshu.com/p/d2fb0fed3b25
https://bbs.pediy.com/thread-217241.htm
https://bbs.pediy.com/thread-252795.htm