PE文件格式详细解析

PE(Portable Executable)文件是Windows操作系统下的一种可执行文件格式。它是Windows系统中常见的二进制文件格式之一,通常包括程序代码、资源、元数据等信息。PE文件最初是在Windows NT操作系统中引入的,并且在Windows 95/98/ME、Windows 2000/XP、Windows Vista/7/8/10等操作系统中得到广泛使用。

Windows 上的 PE 文件采用了 Portable Executable 文件格式,其中包括了 DOS 头、PE 头和多个节区,其中 DOS 头用于兼容 DOS 系统,PE 头是 PE 文件的主要头部,定义了 PE 文件的一些重要属性,例如程序入口点、程序入口代码的 RVA 和大小等等。而 Linux 上的可执行文件则采用了 ELF 文件格式,其中包括了 ELF 头、段表和节表,其中 ELF 头是 ELF 文件的主要头部,定义了 ELF 文件的一些重要属性,例如程序入口点、节表的偏移地址等等。

PE文件由若干个数据结构组成,其中最重要的是PE头部(PE header)和节表(Section table)。PE头部包含了文件的基本信息,例如文件的魔数、机器类型、入口点地址、节表偏移等;而节表则描述了PE文件的各个区段(Section),每个区段通常包含一些代码、数据、资源等信息。

在PE文件中,代码和数据通常被放置在不同的区段中,并且可以通过导入表(Import table)和导出表(Export table)来实现模块间的函数调用和数据传递。PE文件还包括一些其他的数据结构,例如资源表、重定位表、调试信息等,这些结构可以为程序的调试和优化提供支持。

PE文件格式是可扩展的,可以添加自定义的数据结构和字段。因此,许多软件开发工具链和反汇编工具都支持PE文件格式,并提供了丰富的扩展机制,使得PE文件格式在Windows平台下得到了广泛应用。

PE文件格式

下面是常见的PE文件种类、扩展名以及其他关键信息的表格:

文件类型扩展名描述
可执行文件.exe包含可执行程序的代码和数据。
动态链接库(DLL).dll包含可供程序动态链接调用的代码和数据,可以在多个进程间共享。
静态链接库(LIB).lib包含可供程序静态链接调用的代码和数据。
驱动程序(SYS).sys包含设备驱动程序的代码和数据,可以在Windows操作系统中运行。
控制面板应用程序(CPL).cpl包含控制面板应用程序的代码和数据,可以在Windows操作系统中运行。
屏幕保护程序(SCR).scr包含屏幕保护程序的代码和数据,可以在Windows操作系统中运行。
COM对象.ocx包含COM对象的代码和数据,通常用于实现Windows操作系统下的ActiveX控件。

需要注意的是,PE文件的扩展名并不是固定的,有些PE文件可能会使用其他的扩展名,例如:.ocx、.drv、.mui、.fon等。此外,32位和64位的PE文件格式在某些细节上有所不同。

屏幕截图 2023-05-11 170238

如上图,使用二进制编辑工具打开编译好的示例文件可以看到明显的PE文件标识:

  • 文件偏移: 00000000对应的4D 5A (MZ) 对应DOS头的e_magic字段
  • 文件偏移: 00000080对应的50 45 (PE) 对应NT头的Signature字段

image-20230511165817576

PE文件是Windows操作系统下可执行文件的一种格式,包括PE头和PE体两部分。PE头是指DOS头和PE头的结合体,存储了PE文件的基本信息,如文件版本、程序入口地址、可执行文件头的长度、各个节区的位置和大小等。PE体则由多个节区组成,每个节区存储了不同类型的数据,如代码、数据、资源、导入表、导出表等。

在PE文件中,使用偏移(offset)来表示位置,这个位置是指相对于文件开头的偏移量。而在内存中,使用虚拟地址(Virtual Address,VA)来表示位置,这个位置是指相对于进程地址空间的偏移量。当文件被加载到内存时,各节区的大小和位置可能会发生变化,这是因为操作系统需要将文件的节区映射到内存中,并根据操作系统的内存分配方式进行对齐和填充。

常见的节区包括代码节(.text)、数据节(.data)和资源节(.rsrc),它们分别用于保存代码、数据和资源。在PE文件中,PE头和各节区之间存在一个空白区域,也称为NULL填充,用于保证各个节区的对齐。此外,为了保证文件/内存中节区的起始位置处于各文件/内存最小单位的倍数上,可能会使用NULL填充一些空白区域,以确保各个节区的起始位置都能够被正确对齐。

从DOS头到节区头是PE头部分,其下的节区合称为PE体。

偏移:00000000-00000080

PE文件中的偏移量是指从文件头开始到某个特定位置的字节数。在加载PE文件时,操作系统会将整个PE文件映射到进程的虚拟地址空间中。此时,PE文件中的偏移量对应着进程的虚拟地址空间中的一个具体地址。这个具体地址将被用于访问PE文件中的数据。

具体来说,PE文件中的偏移量加上PE文件的基地址就可以得到在内存中的地址。PE文件的基地址是PE文件被加载到内存中时所使用的起始地址。在32位Windows系统中,PE文件的基地址通常是0x400000;在64位Windows系统中,则通常是0x140000000。因此,在32位Windows系统中,一个PE文件中的偏移量为0x1000的位置在内存中的地址是0x401000(0x400000 + 0x1000)。

ASLR(Address Space Layout Randomization)技术是一种操作系统级别的安全措施,旨在增加攻击者利用漏洞进行攻击的难度。ASLR通过随机化进程虚拟地址空间中各个模块、代码段、栈和堆等内存区域的布局,使得攻击者难以确定代码和数据在内存中的确切位置,从而降低了攻击的成功率。

当一个程序启动时,它会被加载到内存中,并分配一块连续的虚拟地址空间来存放其代码和数据。ASLR技术通过在每次启动时随机化分配这些虚拟地址,使得每次程序启动时内存布局都不同。具体来说,ASLR技术会随机化程序、共享库和堆栈等内存区域的基本地址和大小等属性,从而使得攻击者难以准确地计算出要攻击的目标地址。

ASLR技术可以大幅度降低许多常见的攻击,例如缓冲区溢出、ROP(Return-oriented programming)和JIT(Just-In-Time)攻击等。然而,ASLR并不能完全消除所有的安全问题,因为某些漏洞仍然可以通过其他手段或技术来利用。因此,ASLR通常与其他安全措施结合使用,以提高系统的整体安全性。

另外,VA和RVA都是PE文件中的内存地址。VA(Virtual Address)是程序在内存中的地址,而RVA(Relative Virtual Address)是一个节在PE文件中的偏移量。我们通常使用RVA来访问PE文件中的数据或代码,而不直接使用VA地址。

它们之间的计算关系被PE文件头中的ImageBase字段和每个节头部的VirtualAddress字段所决定。

如果我们知道某个节在PE文件中的RVA地址,就可以通过公式VA地址 = ImageBase + RVA地址 - 节头部中的VirtualAddress字段值计算出对应的VA地址。

反之,如果我们知道某个VA地址所属的节,就可以通过公式RVA地址 = VA地址 - ImageBase + 节头部中的VirtualAddress字段值计算出对应的RVA地址。

PE文件执行过程

当操作系统加载并执行一个PE文件时,它会按照下列步骤进行:

  • 加载器定位PE头:当一个PE文件被加载时,Windows会首先定位该文件的PE头。此头包含着该文件的所有必要信息,例如节表、导入表、导出表等等。
  • 加载器解析节表:一旦PE头被定位后,Windows会开始解析该文件的节表。节表是由多个节组成的,每个节都包含着特定的数据。例如代码节(.text)存储着可执行代码,数据节(.data)存储着静态数据等。Windows在加载和执行PE文件时,会根据这些节的属性来对它们进行不同的处理。
  • 加载器分配内存:在解析完节表后,Windows会为该PE文件分配内存空间。该内存空间用于存储该文件的所有节及其数据。
  • 加载器读取节数据:一旦内存空间被分配好了,Windows就会从PE文件中读取每个节的数据,并将其复制到相应的内存区域中。这样,在内存中就得到了与PE文件相同的数据映像。
  • 加载器修复重定位表:由于PE文件可能被编译成与实际内存地址不匹配的形式,所以当PE文件被加载到内存中之后,Windows需要对其中的重定位表进行修复。这样才能保证程序在内存中正确地执行。
  • 加载器处理导入表:当PE文件中包含有对其他动态链接库(DLL)中函数的调用时,Windows会通过导入表来解决这些调用。导入表是一个数组,其中包含了所需的DLL及其函数名。Windows会使用这些信息来将程序与相应的DLL建立联系。
  • 加载器执行入口点:最后,当所有的节都被加载、修复完毕、处理好导入表之后,Windows就会跳转到该PE文件指定的入口点,开始执行程序代码。

image-20230512142028591

时序图如下:

加载器PE文件操作系统打开文件读取头信息分配内存返回内存地址加载区段解析导入表重定位程序加载导入的DLLDLL加载完成填充导入表调用DLL初始化加载完成执行入口点运行主程序调用系统API返回调用结果主线程返回加载器PE文件操作系统

PE头解析

1、DOS头(IMAGE_DOS_HEADER)

下面是PE头中DOS头的内容列表以及它们的用途:

字段名偏移量长度(字节)描述用途
e_magic0x002指定文件类型的标志。 如果是MZ,则为 "MZ",如果是PE,则为 "PE\0\0"用于确定文件格式和类型
e_cblp0x022在文件末尾的最后一个段填充字节数用于确定DOS程序的内存要求
e_cp0x042文件中的段数用于加载和链接程序
e_crlc0x062重定位表条目数用于动态链接
e_cparhdr0x082头的大小(以段为单位)用于确定可执行文件的入口点
e_minalloc0x0A2所需内存的最小值(以字节为单位)用于确定程序所需的内存大小
e_maxalloc0x0C2所需内存的最大值(以字节为单位)用于确定程序所需的内存大小
e_ss0x0E2堆栈段的初始值用于设置堆栈段
e_sp0x102堆栈指针的初始值用于设置堆栈段
e_csum0x122整个文件的校验和(设置为0时不计算)用于验证文件内容的完整性
e_ip0x142程序执行的初始地址用于确定可执行文件的入口点
e_cs0x162代码段的初始值用于加载和链接程序
e_lfarlc0x182重定位表的文件偏移地址用于动态链接
e_ovno0x1A2覆盖号(用于覆盖管理)用于DOS系统管理覆盖
e_res[4]0x1C8保留字段保留未来使用
e_oemid0x242OEM标识符(用于OEM信息)用于指示文件是由哪个OEM制造商创建
e_oeminfo0x262OEM信息(特定于OEM)用于指示文件是由哪个OEM制造商创建
e_res2[10]0x2820保留字段保留未来使用
e_lfanew0x3C4COFF头开始的文件偏移量用于确定COFF头的位置

这些字段组成了DOS头(64字节),提供了有关可执行文件的基本信息,包括文件格式、内存需求、入口点等。在加载和链接程序时,操作系统使用这些信息来正确地加载和执行程序。

在PE头中的DOS头中,有许多字段都是非常重要的,但其中一些比其他字段更加关键。以下是一些关键字段:

  1. e_magic:这个字段指定文件类型的标志,如果是MZ,则为 "MZ",如果是PE,则为 "PE\0\0"。这个字段的值决定了操作系统将如何解析可执行文件。例如,如果e_magic是"MZ",则操作系统会将其视为DOS可执行文件,并使用DOS子系统来运行程序。如果e_magic是"PE\0\0",则操作系统会将其视为PE格式的可执行文件,并使用相应的子系统来加载和运行程序。
  2. e_lfanew:这个字段指定COFF头开始的文件偏移量。COFF头包含有关可执行文件的更多信息,如导出表、导入表、资源表等。e_lfanew的值指示COFF头的相对位置,使操作系统能够找到COFF头并解析其中的数据(NT头的偏移)。
  3. e_cparhdr:这个字段指定头的大小,以段为单位。在PE头中的DOS头之后,紧随着COFF头和节表。e_cparhdr的值指示PE头的相对位置,使操作系统可以正确地加载和链接程序。
  4. e_ip:这个字段指定程序执行的初始地址。当操作系统加载程序时,它将从该地址开始执行程序。这个地址通常是程序的入口点,即程序开始执行的第一条指令的地址。如果e_ip的值不正确,程序将无法正常启动。
  5. e_cs:这个字段指定代码段的初始值。操作系统使用e_cs和e_ip来确定程序执行的第一条指令的地址。
  6. e_sp:这个字段指定堆栈指针的初始值。操作系统使用e_ss和e_sp来设置堆栈段。

这些字段对于可执行文件的正确加载和执行至关重要。如果这些值不正确,程序将无法正常启动,或者可能会导致崩溃或其他错误。因此,在创建可执行文件时,必须确保这些关键字段都正确地设置。

2、DOS 存根(Dos stub)

image-20230512100821332

在PE文件中,DOS stub(也称为MS-DOS stub)是一小段DOS程序代码,它出现在PE文件头的最前面,用于兼容早期的MS-DOS操作系统。DOS stub通常由16位汇编语言编写,其作用是在运行PE文件之前,向用户显示一些自定义的信息或错误提示。

DOS stub的存在是为了保证PE文件在早期的DOS操作系统中仍然能够被识别和执行,因为这些旧的操作系统可能不支持PE文件格式。当用户在旧版的DOS操作系统中执行PE文件时,DOS stub会被首先加载并执行,然后才会跳转到PE文件的真正入口点(Entry Point)执行PE程序。

DOS stub通常非常小,一般只有几十个字节大小,因为它只需要包含一些简单的汇编代码和一些用于显示信息的字符串即可。在Windows中,可以使用工具如Editbin或者其他二进制编辑器来修改或删除DOS stub。但需要注意的是,如果DOS stub被删除或修改,那么在旧版DOS操作系统中执行该PE文件时可能会出现问题,因此修改DOS stub需要谨慎操作。

3、NT 头(IMAGE_NT_HEADERS)

字段名字段含义字段类型字段长度备注
SignatureNT头部的标识DWORD4字节固定值为0x00004550('PE\0\0')
FileHeader文件头IMAGE_FILE_HEADER 结构体20字节
OptionalHeader可选头部IMAGE_OPTIONAL_HEADER 结构体224字节(32位PE文件)/ 240字节(64位PE文件)

下面对表格中的每个字段再进行详细说明:

1、Signature

Signature字段是一个4字节的固定值,用于识别NT头部的开始位置。对于PE文件,它的值是0x00004550,也就是ASCII码下的"PE"两个字母。

2、标准文件头(IMAGE_FILE_HEADER)

下面是IMAGE_FILE_HEADER结构体的表格:

字段名字段含义字段类型字段长度备注
Machine机器类型WORD2字节表示该PE文件是在哪种类型的机器上编译生成的。
NumberOfSections节区数量WORD2字节表示该PE文件中包含了多少个节区。
TimeDateStamp时间戳DWORD4字节表示该PE文件生成的时间。
PointerToSymbolTable符号表偏移地址DWORD4字节表示该PE文件符号表的起始地址。
NumberOfSymbols符号表大小DWORD4字节表示该PE文件符号表的大小。
SizeOfOptionalHeader可选头部大小WORD2字节表示该PE文件可选头部的大小。
Characteristics文件属性标志WORD2字节表示该PE文件的属性信息,如是否是可执行文件、是否包含调试信息等。
Machine

Machine字段是一个2字节的无符号整数,表示该PE文件是在哪种类型的机器上编译生成的。这个字段的值由枚举类型IMAGE_FILE_MACHINE定义,包括:

  • IMAGE_FILE_MACHINE_UNKNOWN:未知类型的机器。
  • IMAGE_FILE_MACHINE_I386:Intel 386或更高版本的CPU。
  • IMAGE_FILE_MACHINE_ARM:ARM架构的CPU。
  • IMAGE_FILE_MACHINE_AMD64:64位的x86架构CPU。
NumberOfSections

NumberOfSections字段是一个2字节的无符号整数,表示该PE文件中包含了多少个节区。

TimeDateStamp

TimeDateStamp字段是一个4字节的无符号整数,表示该PE文件生成的时间。这个时间是从1970年1月1日起到生成该PE文件的时间所经过的秒数。

PointerToSymbolTable

PointerToSymbolTable字段是一个4字节的无符号整数,表示该PE文件符号表的起始地址。如果该PE文件没有符号表,则该字段为0。

NumberOfSymbols

NumberOfSymbols字段是一个4字节的无符号整数,表示该PE文件符号表的大小。如果该PE文件没有符号表,则该字段为0。

SizeOfOptionalHeader

SizeOfOptionalHeader字段是一个2字节的无符号整数,表示该PE文件可选头部的大小。对于32位PE文件,该字段的值为224字节;对于64位PE文件,该字段的值为240字节。

Characteristics

Characteristics字段是一个2字节(16位)的无符号整数,表示该PE文件的属性信息,如是否是可执行文件、是否包含调试信息等。这个字段的值由枚举类型IMAGE_FILE_CHARACTERISTICS定义,具体字段如下表:

字段名描述
IMAGE_FILE_RELOCS_STRIPPED是否剥离了重定位信息。如果这个标志位被设置,则说明在PE文件被加载时,所有的重定位信息都应该被省略掉。
IMAGE_FILE_EXECUTABLE_IMAGE是否是可执行文件。
IMAGE_FILE_LINE_NUMS_STRIPPED是否剥离了行号信息。如果这个标志位被设置,则说明在PE文件被加载时,所有的行号信息都应该被省略掉。
IMAGE_FILE_LOCAL_SYMS_STRIPPED是否剥离了局部符号信息。如果这个标志位被设置,则说明在PE文件被加载时,所有的局部符号信息都应该被省略掉。
IMAGE_FILE_AGGRESSIVE_WS_TRIM是否采用了积极的工作集修剪策略。如果这个标志位被设置,则说明在PE文件被加载时,该PE文件所占用的内存会被尽可能快地释放出来。
IMAGE_FILE_LARGE_ADDRESS_AWARE是否支持2GB以上的地址空间。如果这个标志位被设置,则说明该PE文件可以使用2GB以上的地址空间。
IMAGE_FILE_BYTES_REVERSED_LO低字节序的字节顺序是否被颠倒。如果这个标志位被设置,则说明该PE文件的低字节序的字节顺序被颠倒了。
IMAGE_FILE_32BIT_MACHINE是否是32位机器的可执行文件。如果这个标志位被设置,则说明该PE文件是32位机器的可执行文件。
IMAGE_FILE_DEBUG_STRIPPED是否剥离了调试信息。如果这个标志位被设置,则说明在PE文件被加载时,所有的调试信息都应该被省略掉。
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP是否可从可移动介质上运行,且可在运行时将该PE文件的映像转移到交换文件中。
IMAGE_FILE_NET_RUN_FROM_SWAP是否可从网络上运行,且可在运行时将该PE文件的映像转移到交换文件中。
IMAGE_FILE_SYSTEM是否是系统文件。如果这个标志位被设置,则说明该PE文件是一个系统文件。
IMAGE_FILE_DLL是否是DLL文件。如果这个标志位被设置,则说明该PE文件是一个DLL文件。
IMAGE_FILE_UP_SYSTEM_ONLY该PE文件是否仅能在高版本的操作系统上运行。如果这个标志位被设置,则说明该PE文件只能在高版本的操作系统上运行。
IMAGE_FILE_BYTES_REVERSED_HI高字节序的字节顺序是否被颠倒。如果这个标志位被设置,则说明该PE文件的高字节序被颠倒了

手工查看

image-20230512104933127

工具解析

image-20230512105057464

上述两张图片展示了手工查看及工具解析的结果

3、拓展文件头(IMAGE_OPTIONAL_HEADER32)

由于拓展头字段较多,这里只列举出比较重要的字段。以下是PE文件头中的Optional Header中比较重要的字段:

字段名描述
Magic指定Optional Header的版本号。
AddressOfEntryPoint指定程序入口点相对于映像基地址的偏移量。
ImageBase指定该映像的首选装载地址。
SectionAlignment指定内存中的节的对齐方式。
FileAlignment指定PE文件中的各个节在文件中的对齐方式。
SizeOfImage指定整个映像的大小,包括所有的头和节对齐后所占据的空间。
Subsystem指定该映像所需要的子系统。(区分系统驱动文件和普通可执行文件)
SizeOfStackReserve指定要为进程的主线程栈保留的字节数。
SizeOfStackCommit指定要为进程的主线程栈提交的字节数。
SizeOfHeapReserve指定要为进程的堆保留的字节数。
SizeOfHeapCommit指定要为进程的堆提交的字节数。
NumberOfRvaAndSizes指定数据目录表的数量。
DataDirectory描述可选数据目录表的数组。

其中,Magic字段指定Optional Header的版本号,用于确定Optional Header的格式。AddressOfEntryPoint字段指定程序的入口点地址相对于映像基地址的偏移量。ImageBase字段指定映像的首选装载地址,用于指示映像在内存中的位置。SectionAlignment字段指定节在内存中对齐的方式,FileAlignment字段指定节在PE文件中对齐的方式。SizeOfImage字段指定整个映像的大小,包括所有的头和节对齐后所占据的空间。Subsystem字段指定映像所需要的子系统,比如Windows GUI、Windows Console等。SizeOfStackReserve和SizeOfStackCommit字段分别指定为进程的主线程栈保留和提交的字节数。SizeOfHeapReserve和SizeOfHeapCommit字段分别指定为进程的堆保留和提交的字节数。NumberOfRvaAndSizes字段指定数据目录表的数量。DataDirectory字段描述可选数据目录表的数组。

image-20230512110145948

下面是Subsystem字段的常见取值及对应的含义,以及它们的16进制表示:

取值含义HEX值
IMAGE_SUBSYSTEM_UNKNOWN未知子系统。0
IMAGE_SUBSYSTEM_NATIVE本地子系统。1
IMAGE_SUBSYSTEM_WINDOWS_GUIWindows GUI子系统。2
IMAGE_SUBSYSTEM_WINDOWS_CUIWindows控制台子系统。3
IMAGE_SUBSYSTEM_OS2_CUIOS/2控制台子系统。5
IMAGE_SUBSYSTEM_POSIX_CUIPOSIX控制台子系统。7
IMAGE_SUBSYSTEM_WINDOWS_CE_GUIWindows CE GUI子系统。9
IMAGE_SUBSYSTEM_EFI_APPLICATIONEFI应用程序子系统。10 (0xA)
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVEREFI启动服务驱动子系统。11 (0xB)
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVEREFI运行时驱动子系统。12 (0xC)
IMAGE_SUBSYSTEM_XBOXXbox子系统。14 (0xE)
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATIONWindows引导应用程序子系统。16 (0x10)

还有一个比较重要的DataDirectory(可选数据目录表),DataDirectory是一个结构体数组,每个结构体描述了一个数据目录。这些数据目录描述了PE文件中各种数据的位置和大小,包括导入表、导出表、重定位表等等。

每个DataDirectory结构体包含两个字段,分别是VirtualAddress和Size,它们的含义如下:

  • VirtualAddress:数据目录的虚拟地址,即数据目录在内存中的位置。
  • Size:数据目录的大小,以字节为单位。

PE文件中的DataDirectory数组总共包含16个元素,每个元素描述一个数据目录,其定义如下:

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

下面是PE文件中16个数据目录的定义及其含义:

数据目录索引数据目录名称含义
0IMAGE_DIRECTORY_ENTRY_EXPORT导出表
1IMAGE_DIRECTORY_ENTRY_IMPORT导入表
2IMAGE_DIRECTORY_ENTRY_RESOURCE资源表
3IMAGE_DIRECTORY_ENTRY_EXCEPTION异常表
4IMAGE_DIRECTORY_ENTRY_SECURITY安全表
5IMAGE_DIRECTORY_ENTRY_BASERELOC重定位表
6IMAGE_DIRECTORY_ENTRY_DEBUG调试信息表
7IMAGE_DIRECTORY_ENTRY_COPYRIGHT版权信息表
8-9IMAGE_DIRECTORY_ENTRY_GLOBALPTR、IMAGE_DIRECTORY_ENTRY_TLS保留,一般为0
10IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG加载配置表
11IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT延迟加载导入表
12-15保留一般为0

image-20230512111526352

需要注意的是,PE文件中的DataDirectory描述的是数据在文件中的位置,而不是实际加载到内存中的位置。在加载PE文件时,系统会将这些数据加载到内存中,然后根据需要对其进行修复和重定位。

4、节区头

在PE文件中,节区头(Section Header)是用来描述PE文件中每个节(Section)的数据结构。节是PE文件中存储代码、数据和资源的基本单元,通常对应于程序中的代码段、数据段、BSS段等。PE文件中的节区头提供了每个节的信息,如名称、大小、属性等,便于系统加载和执行。

在32位PE文件中,节区头的定义如下:

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

各字段的含义如下:

  • Name:节区名称,8字节长,若名称长度不足8字节则用null补齐。
  • Misc.PhysicalAddress 或 Misc.VirtualSize:在不同情况下有不同的含义,PhysicalAddress是文件中的起始地址,VirtualSize是内存中的大小。
  • VirtualAddress:节的虚拟地址,即在内存中的位置。(内存中节区起始地址(RVA))
  • SizeOfRawData:节的大小,以字节为单位,包括节中实际存储的内容以及可能存在的对齐填充。(磁盘文件中节区所占大小)
  • PointerToRawData:节在文件中的起始位置,以字节为单位。
  • PointerToRelocations:节中重定位表的位置,以文件偏移量为单位,若没有重定位表则为0。
  • PointerToLinenumbers:节中行号表的位置,以文件偏移量为单位,若没有行号表则为0。
  • NumberOfRelocations:重定位表中重定位项的数目,若没有则为0。
  • NumberOfLinenumbers:行号表中行号的数目,若没有则为0。
  • Characteristics:节的属性,包括可读、可写、可执行、初始化数据等信息。每个属性用一个标志位表示,详细信息可见前面提到的IMAGE_SCN枚举。

需要注意的是,节区头的数量和每个节区头的大小是固定的,都是40个字节(IMAGE_SIZEOF_SECTION_HEADER)。节区头直接跟在PE头后面,每个节区头描述了一个节的信息。因此,根据PE头中的NumberOfSections字段可以计算出PE文件中有多少个节,从而获得文件中所有节的信息。

image-20230512113203678

以下是Charateristics每一位的含义以及对应的标记:

位数标志含义
0IMAGE_SCN_TYPE_NO_PAD节没有填充字节。
1IMAGE_SCN_CNT_CODE包含可执行代码。
2IMAGE_SCN_CNT_INITIALIZED_DATA包含初始化数据(已经在编译时给出初始值)。
3IMAGE_SCN_CNT_UNINITIALIZED_DATA包含未初始化数据(不具有初始值)。
4IMAGE_SCN_LNK_INFO链接器使用的信息节。
5IMAGE_SCN_LNK_REMOVE(Obsolete)Obsolete标志,现在不再使用。
6-7IMAGE_SCN_LNK_COMDAT用于指示COMDAT节的链接类型。
8IMAGE_SCN_GPREL包含相对于全局指针的地址。
9IMAGE_SCN_MEM_PURGEABLE可以通过内存回收工具进行回收。
10IMAGE_SCN_MEM_16BIT(Obsolete)Obsolete标志,现在不再使用。
11IMAGE_SCN_MEM_LOCKED保留,不使用。
12IMAGE_SCN_MEM_PRELOAD在静态链接时预加载节。
13IMAGE_SCN_ALIGN_1BYTES按1字节对齐。
14IMAGE_SCN_MEM_EXECUTE可执行。
15IMAGE_SCN_MEM_READ可读。
16IMAGE_SCN_MEM_WRITE可写。
17IMAGE_SCN_LNK_NRELOC_OVFL节包含太多重定位项,链接器会进行优化。
18IMAGE_SCN_MEM_DISCARDABLE可以被丢弃(释放)的节。
19-20IMAGE_SCN_MEM_NOT_CACHED用于指示节不应缓存或不可快速访问。
21IMAGE_SCN_MEM_NOT_PAGED不可以换页的节。
22IMAGE_SCN_MEM_SHARED节可以被共享(当做DLL使用)。
23IMAGE_SCN_MEM_EXECUTE_AND_READ能够被执行和读取。
24IMAGE_SCN_MEM_EXECUTE_AND_WRITE能够被执行和写入。
25-26IMAGE_SCN_MEM_READ_AND_WRITE可读可写。
27-28IMAGE_SCN_MEM_SHARED_AND_EXECUTE可共享和可执行。
29-31Reserved保留,不使用。

image-20230512134001760

上图即是示例PE文件中的.text节表的节属性字段解析对应

5、RVA To RAW转换

RVA(Relative Virtual Address)指的是相对虚拟地址,是指一个节区(section)内的相对地址。在Windows系统中,PE文件中的许多数据结构中都使用了RVA,例如导出表(export table)、导入表(import table)、资源表(resource table)等。

但是,在实际的执行过程中,系统需要的是真实的物理磁盘上的偏移地址,也就是RAW地址(也叫文件偏移地址)。因此,当系统需要访问某个RVA的数据时,需要将其转换为RAW地址。

RVA和RAW之间的转换需要考虑两个因素:首先,PE文件中的各个节区是按照文件偏移地址顺序排列的,因此节区的偏移地址和大小都是已知的;其次,每个节区都有一个对齐方式(对齐大小),用于将数据对齐到文件偏移地址的某个位置上。

以下是几个转换公式:

RAW地址 = RVA - 节区RVA + 节区RAW地址


RAW = RVA - ImageBase + PointerToRawData
其中,ImageBase指的是PE文件的装载地址,PointerToRawData指的是该节区的文件偏移地址。这个公式的含义是,将RVA转换为RAW地址时,需要减去PE文件的装载地址(ImageBase),并加上该节区的文件偏移地址(PointerToRawData),以得到真实的文件偏移地址。


RAW - PointerToRawData = RVA - ImageBase
这个公式与上一个公式的含义是相同的,只是做了一些简化处理,将公式两边都减去PointerToRawData,这样得到的就是该节区的偏移量,也就是该节区起始位置的偏移量。这样计算出来的值是相对于节区起始位置的偏移量,因此需要加上节区的起始地址才能得到最终的RAW地址。

其中,节区RVA和节区RAW地址可以从节区头中获取,RVA是要转换的相对虚拟地址。在转换时,需要先找到包含该RVA的节区,然后使用上述公式进行计算即可。

需要注意的是,如果某个RVA跨越了多个节区,那么需要根据所在的节区进行拆分计算,最终将结果相加得到最终的RAW地址。此外,在计算RAW地址时,还需要考虑节区的对齐方式,以保证转换后的RAW地址是对齐的。

最后修改:2024 年 01 月 12 日
如果觉得我的文章对你有用,请随意赞赏