NT 头部(NT Header)是 PE 文件格式的核心部分之一,它包含了有关程序如何加载、执行以及一些重要的文件属性。NT 头部常被认为是 PE 头部 的核心或“真正的”PE 头部,因为操作系统加载 PE 文件时,首先会查找 DOS 头部的 e_lfanew
字段,定位到 NT 头部,即 PE 文件的有效头部(这也是我在上一篇文章《PE文件结构-PE文件结构-DOS头部&DOS stub》中的PE文件结构部分NT header
为PE头部的原因)。
但实际上,在 PE 文件格式(Portable Executable format)中,PE 头部 和 NT 头部 是同一个概念的不同部分,实际上 NT 头部 是 PE 头部 的一个子结构。它们共同定义了可执行文件的加载、执行方式以及其它重要信息。PE 头部包含了从 DOS 头部开始到 节表(Section Header)部分之间的所有内容,而 NT 头部则是 PE 头部的核心部分,负责描述文件的结构和属性。
NT 头部具体包括:PE 标识符(PE Signature)、文件头(File Header)、可选头(Optional Header)这些部分共同构成了 NT 头部,在 PE 文件结构中起着关键作用。下面我会分别介绍这三个部分:
NT头部的结构
①PE 标识符(PE Signature)
PE 标识符通常位于 NT 头部的最前面,它是NT头部的第一个字段是一个标识符,用来标识该文件为PE格式,它是一个4字节的值,通常是字符串PE\0\0。这个签名用于告诉操作系统该文件是一个有效的PE文件。
②文件头(File Header)
文件头(File Header)是 NT 头部的第一部分,包含了 PE 文件的一些基本信息,描述了目标机器的类型、节的数量、文件的创建时间等。
③可选头(Optional Header)
可选头包含了更详细的文件加载和执行信息,实际上是 PE 文件中最重要的一部分,尽管名字中有“可选”字样,但它是必须的。可选头中包含了程序的入口点地址、图像基地址、节的对齐要求、堆栈大小等信息,操作系统通过这些信息来正确加载和执行程序。
接着通过Visual Studio
来进一步查看NT
头部的结构,查看方式与前一篇文章《DOS头部&DOS stub》中描述的一样,在随意的一个C/Cpp
文件中,敲入NT头部结构体:IMAGE_NT_HEADERS
接着按住ctrl
键点击结构体,查看结构体内部结构:
typedef struct _IMAGE_NT_HEADERS {DWORD Signature;IMAGE_FILE_HEADER FileHeader;IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
可以看到NT头部中就包含有上述介绍的PE 标识符(PE Signature)、文件头(File Header)、可选头(Optional Header)。这个时候可以通过010Editor进一步查看文件的结构此处的程序使用的是上一篇文章中生成的程序
:
在DOS头部、DOS stub后的内容就是NT头部;那么结合上述NT头部的结构,就可以很清楚地看出在NT头部开头的4个字节就是PE Signature
:
而PE标识是不允许更改的,若我们更改了PE标识,那么此时程序就没法运行了:
接着我们往下继续说一下NT头部中的其他内容。
文件头(FileHeader)结构
在PE(Portable Executable)文件格式中,FileHeader 是 NT 头部的一部分,包含了关于文件的基本元数据,FileHeader 结构描述了文件的基本信息,如文件类型、机器架构、节区数量、时间戳等内容,它位于 NT 头部中的第二部分,在 IMAGE_NT_HEADERS
结构中。
长按ctrl
点击NT头部中的IMAGE_FILE_HEADER
进行跳转,查看文件头部的内容:
typedef struct _IMAGE_FILE_HEADER {WORD Machine;WORD NumberOfSections;DWORD TimeDateStamp;DWORD PointerToSymbolTable;DWORD NumberOfSymbols;WORD SizeOfOptionalHeader;WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
①Machine (WORD)
该字段指示目标机器的架构类型,即PE文件所支持的硬件平台;这个时候在FILE_HEADER
结构体所在位置往下拉,我们就可以看到MACHINE
字段的取值:
具体的取值如下:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE
这个时候我们在010Editor中的位置,找到实例文件的MACHINE
取值:014C
(小端序存储)
那么通过上述我们就可以直接知道目标机器的架构类型为:Intel 386
;这个值代表了该 PE 文件是针对 32 位 x86 架构(即 i386 处理器)的目标平台。
②NumberOfSections(WORD)
该字段指定PE文件中节区(section)的数量。PE文件由多个节区组成,每个节区存储了特定类型的数据或代码(例如 .text
、.data
、.rsrc
等)。在010 Editor
中可以看到该示例文件的值为00 09
:
如果一个 PE 文件的 NumberOfSections
字段的值为 9,这表示该文件包含 9 个节,每个节可能代表不同的程序部分。例如,文件可能包含以下节:
①.text — 代码段
②.data — 数据段
③.rdata — 只读数据段
④.bss — 未初始化数据段
⑤.edata — 导出符号表
⑥.idata — 导入符号表
⑦.rsrc — 资源段
⑧.reloc — 重定位表
⑨.debug — 调试信息(可选)
NumberOfSections
也不能随意修改:在 PE 文件中,每个节都有一个对应的节头(IMAGE_SECTION_HEADER
)。节头结构包含有关该节的元数据(例如节的名称、大小、内存地址等),NumberOfSections
字段指示节头的数量,因此,如果你修改 NumberOfSections
,你必须确保节头的数量也正确更新。
③TimeDateStamp(DWORD)
该字段表示文件的时间戳。它通常是编译或链接时生成的时间,表示PE文件创建的具体时刻;时间戳的值是一个32位的时间戳,表示自1970年1月1日起的秒数。010Editor中的位置:
该字段的修改并不会影响程序的运行:
④PointerToSymbolTable(DWORD)
该字段是指向符号表的指针,但在现代Windows程序中通常不再使用,因为符号表主要用于调试信息。在010 Editor中查看该字段位置:
该字段的修改也不会影响程序的运行:
在旧版的PE文件(例如Windows 95及早期)中,符号表包含了调试符号,帮助开发者进行反汇编和调试。现代的PE文件通常不会使用该字段,且符号信息通常与程序文件分离(例如使用.pdb文件存储符号)。
⑤NumberOfSymbols(DWORD)
指定符号表中的符号数量;与PointerToSymbolTable
字段结合使用,表示符号表中存储的符号数量。在现代PE文件中,这个字段通常被设为零,因为不再使用符号表。查看其位置,并以CC
字符填充该字段:
再次运行程序,发现字段中值的修改并不影响程序的正常运行。
⑥SizeOfOptionalHeader(WORD)
该字段表示可选头(Optional Header)的大小,可选头包含了程序的更多信息,如入口点、映像基址、堆栈大小等。除此之外,PE文件的加载和执行相关的关键信息都存储也存储在可选头中。
该字段不可随意修改,CC字符填充后运行程序:
⑦Characteristics(WORD)
该字段表示PE文件的特性,用于描述文件的类型、功能和运行要求。该字段的值是一个位掩码,每一位代表不同的特性。该字段的具体取值在该结构体下面被定义:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved external references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Aggressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
具体位置如下:
看到该字段的值为:01 02
这个时候通过对照上面的取值列表(0x0102 = 0x0100 + 0x0002
)可以知道:该程序为一个32位的可执行程序。
同样的,该字段不可随意修改:
至此NT头部
中的文件头部已经全部介绍完毕,接着就对NT头部的可选头部(OptionalHeader
)进行说明:
可选头(OptonalHeader)结构
同样的,在VS中按住ctrl
点击结构体进行跳转:
typedef struct _IMAGE_OPTIONAL_HEADER {WORD Magic;BYTE MajorLinkerVersion;BYTE MinorLinkerVersion;DWORD SizeOfCode;DWORD SizeOfInitializedData;DWORD SizeOfUninitializedData;DWORD AddressOfEntryPoint;DWORD BaseOfCode;DWORD BaseOfData;DWORD ImageBase;DWORD SectionAlignment;DWORD FileAlignment;WORD MajorOperatingSystemVersion;WORD MinorOperatingSystemVersion;WORD MajorImageVersion;WORD MinorImageVersion;WORD MajorSubsystemVersion;WORD MinorSubsystemVersion;DWORD Win32VersionValue;DWORD SizeOfImage;DWORD SizeOfHeaders;DWORD CheckSum;WORD Subsystem;WORD DllCharacteristics;DWORD SizeOfStackReserve;DWORD SizeOfStackCommit;DWORD SizeOfHeapReserve;DWORD SizeOfHeapCommit;DWORD LoaderFlags;DWORD NumberOfRvaAndSizes;IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
①Magic(WORD)
该字段用于标识PE文件的类型;对于32位的PE文件,其值通常是0x010B
,而对于64位的PE文件,其值是0x020B
。这个时候在010 Editor中查看样例程序中该字段的位置以及值:
由此可以直接得出该程序位32位得PE文件。同样,该字段不可随意修改:
②MajorLinkerVersion(Byte)和MinorLinkerVersion(Byte)
这两个字段分别表示链接器的主版本号和副版本号,用于标识创建PE文件的链接器版本。这两个字段通常与生成 PE 文件的工具(例如 Microsoft Visual Studio 的 link.exe
)的版本相关,帮助标识链接器的版本,通常用于追踪文件生成的工具和版本。
1.主版本号(MajorLinkerVersion):这个字段表示链接器的主要版本。它通常用于标识链接器的大版本更新,这些更新可能包含不兼容的更改或重要的新功能。例如,如果链接器的版本从10升级到11,这将通过主版本号的变化来反映。 2.副版本号(MinorLinkerVersion):这个字段表示链接器的次要版本或修订号。它用于标识链接器的小版本更新,这些更新通常是向后兼容的,可能包含错误修复、性能改进或小的新功能。例如,从10.0到10.1的更新将通过副版本号的变化来反映。
不同版本的 Microsoft Visual Studio 和 link.exe 会生成不同的版本号。例如:
-
Visual Studio 2005 (VS 8.0):链接器版本号为 8.0。
-
Visual Studio 2010 (VS 10.0):链接器版本号为 10.0。
-
Visual Studio 2015 (VS 14.0):链接器版本号为 14.0。
-
Visual Studio 2019 (VS 16.0):链接器版本号为 14.0 或更高(具体版本取决于修订)
MajorLinkerVersion = 0x0E (14) MinorLinkerVersion = 0x01 (1)
在 样例程序的PE 文件结构 可选头(IMAGE_OPTIONAL_HEADER
) 中可以看出MajorLinkerVersion
和 MinorLinkerVersion
分别为 0x0E(14)和 0x01(1),这意味着该文件是使用 link.exe
链接器版本 14.1 生成的。Microsoft 的 link.exe 工具会随着 Visual Studio 版本的变化而更新,因此 MajorLinkerVersion 和 MinorLinkerVersion 会根据 Visual Studio 的版本有所不同。以下是一些常见的示例:
1. 版本 8.0(Visual Studio 2005)MajorLinkerVersion = 8MinorLinkerVersion = 0 2. 版本 10.0(Visual Studio 2010)MajorLinkerVersion = 10MinorLinkerVersion = 0 3. 版本 14.0(Visual Studio 2015)MajorLinkerVersion = 14MinorLinkerVersion = 0 4. 版本 14.28(Visual Studio 2019)MajorLinkerVersion = 14MinorLinkerVersion = 28
最后,在Editor中使用CC字符填充这两个字段,发现这两个字段并不影响程序的正常运行:
③SizeOfCode(DWORD)、SizeOfInitializedData(DWORD)和SizeOfUninitializedData(DWORD)
这三个DWORD类型的字段分别表示代码段、初始化数据和未初始化数据的大小。
Ⅰ.SizeOfCode
:在PE文件的可选头(Optional Header)中,SizeOfCode字段表示代码段(也称为.text段)的总大小。代码段包含了程序的可执行指令。这个字段的单位是字节。它表示程序中所有代码段的总字节数,包括任何填充(padding)数据,以确保内存对齐。
SizeOfCode
在文件结构中的对应位置:
该字段的值为:00 05 84 00
,随意修改该字段的值,发现该字段并不影响程序正常运行。
Ⅱ.SizeOfInitializedData:
在PE文件的可选头(Optional Header)中,SizeOfInitializedData字段表示已初始化数据段(即.data段)的总大小。已初始化数据段包含了程序的全局变量和静态变量,这些变量在程序加载到内存时已经有初始值。该字段的在文件结构中的位置:
该字段的值为:00 01 20 00
,随意修改该字段的值,发现该字段并不影响程序正常运行。
Ⅲ.SizeOfUninitializedData:
在PE文件的可选头(Optional Header)中,SizeOfUninitializedData字段表示未初始化数据段(即.bss段)的总大小。未初始化数据段包含程序中在定义时没有被赋予初始值的全局变量和静态变量。该字段的在文件结构中的位置:
该字段的值为:00 00 00 00
,随意修改该字段的值,发现该字段并不影响程序正常运行。
④AddressOfEntryPoint(DWORD)
AddressOfEntryPoint
字段是PE文件结构中可选头(IMAGE_OPTIONAL_HEADER)的一个重要成员,它定义了程序执行的入口点。这是一个DWORD类型的字段,表示程序的入口点在内存中的相对虚拟地址(RVA)。AddressOfEntryPoint
字段指示程序执行的起始点,当PE文件被加载到内存中时,这个字段指定了操作系统应该首先执行的代码的位置。对于EXE文件,这通常是WinMain
或main
函数的地址;对于DLL文件,则是DllMain
函数的地址。
什么是相对虚拟地址(RVA)?
RVA,全称为Relative Virtual Address(相对虚拟地址),是PE(Portable Executable,可移植执行文件)格式中用于指定地址的一种方式。在PE文件中,RVA是一种相对于PE文件的基地址(ImageBase)的偏移量,用于在内存中定位各个部分(如代码、数据、资源等)的位置。
基地址(ImageBase):PE文件在内存中的首选加载地址称为基地址。对于大多数可执行文件,默认的基地址是0x00400000,但这个值可以在PE文件的可选头中被修改;基地址在PE文件结构中的位置如下。
计算实际地址:AddressOfEntryPoint
是一个RVA值,它是相对于PE文件的ImageBase
(镜像基址)的偏移量。因此,要计算入口点在内存中的绝对地址,需要将ImageBase
与AddressOfEntryPoint
相加。例如,如果ImageBase
是0x00400000
,而AddressOfEntryPoint
是0x00001000
,则程序入口的虚拟地址(VA)为0x00401000
。
此时我的样例程序的AddressOfEntryPoint
值为0002BC565
,ImageBase
的值为00400000
程序入口的虚拟机地址 = ImageBase+ AddressOfEntryPoint
程序入口的虚拟地址
=00400000
+0002BC56
==0042BC56
,这个时候我们就得到了程序的入口点在内存中的实际地址;但是需要注意的一点是如果程序和操作系统中都启用了地址空间布局随机化(ASLR),则ImageBase
会在每次执行时随机化,那么此时计算得到的程序入口地址就不准确了。这个程序入口的虚拟地址是可以修改的,后续可以再单独写篇文章。
程序执行中的ASLR功能的条件:
①操作系统启用ASLR: 操作系统需要全局启用ASLR(如Windows、Linux等),否则即使程序支持ASLR,操作系统也不会进行地址布局随机化。 ②程序支持ASLR: 程序需要在编译时明确启用ASLR支持(如Windows中的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE标志,或者Linux中的PIE支持)。
Visual Studio
生成程序(链接)时决定程序是否支持ASLR
功能的开关:
右击项目-->选择属性-->链接器-->高级-->随机基址
⑤BaseOfCode(DWORD)和BaseOfData(DWORD)
这两个字段分别表示代码段和数据段在内存中的基地址
Ⅰ.BaseOfCode:BaseOfCode
字段是一个DWORD值,它指向代码段(通常称为.text段)在内存中的起始相对虚拟地址(RVA)。也就是说,当PE文件被加载到内存中时,BaseOfCode
就是代码段的开始地址。
Ⅱ.BaseOfData:BaseOfData
字段也是一个DWORD值,它指向数据段(通常称为.data段)在内存中的起始相对虚拟地址(RVA)。对于32位PE文件,这个字段表示数据段的开始地址,而在64位PE文件中,这个字段被合并到ImageBase
字段中,因此64位PE文件中不存在BaseOfData
字段。
随意修改这两个字段,发现程序正常运行
⑥ImageBase(DWORD)
ImageBase字段表示PE文件在内存中的默认加载地址。换句话说,当操作系统加载一个可执行文件或DLL时,它会尝试将其映射到这个指定的虚拟内存地址。如果这个地址已经被其他资源占用,操作系统会选择另一个合适的地址并进行重定位(Relocation)。通常情况下,可执行文件的默认地址为0x00400000,而DLL文件则通常位于更高的地址,如0x10000000。开发者可以在编译时指定这个地址。
右击项目-->选择属性-->链接器-->高级-->固定基址
该字段在上面介绍AddressOfEntryPoint
时已经介绍到了,这边就不做更多赘述。该字段的位置:
该字段无法随意更改:
⑦SectionAlignment(DWORD)和FileAlignment(DWORD)
SectionAlignment
SectionAlignment
字段定义了节(Section)在内存中的对齐边界,它告诉操作系统在加载PE文件时,节的开始位置应该对齐到内存中的多大边界上。具体来说,SectionAlignment
指定了内存中加载节时需要遵循的对齐规则。这个值影响程序加载到内存后,如何将文件中的不同节(如代码段、数据段等)映射到内存中。通常,这个值通常设置为操作系统的页面大小,常见的如 0x1000(即 4KB)。
当程序被加载到内存时,节的实际内存地址应该是 SectionAlignment
倍数的地址,也就是说,操作系统会确保每个节的起始地址与 SectionAlignment
对齐。例如,若 SectionAlignment
为 0x1000
,则程序中的每个节(Section)在内存中的起始地址将是 0x1000
的倍数(如 0x1000
, 0x2000
, 0x3000
等)。
该字段不可以随意修改:
FileAlignment
FileAlignment
这 个值指定了在PE文件的磁盘上,每个节(Section)数据在文件中存储时应该遵循的对齐规则。换句话说,它告诉操作系统文件中的节数据在文件中的存储位置应该如何对齐。这个字段决定了节在PE文件中的存储格式,以及如何优化文件大小或读取性能。FileAlignment
值通常会小于或等于 SectionAlignment
,因为文件本身并不一定需要像内存那样严格地对齐。
当PE文件的节(Section)被写入磁盘时,它们的起始地址将会对齐到 FileAlignment
的倍数。这意味着节在文件中的位置会遵循 FileAlignment
的对齐规则。例如,若 FileAlignment
为 0x200
,那么每个节在文件中的起始地址将是 0x200
的倍数(如 0x200
, 0x400
, 0x600
等)。
同样的,该字段无法随意修改:
⑧MajorOperatingSystemVersion(WORD)和MinorOperatingSystemVersion(WORD)
Ⅰ.MajorOperatingSystemVersion
指定程序所要求的操作系统的主版本号。操作系统的主版本号通常是指操作系统的主要版本,通常对应操作系统的系列版本。例如,Windows XP、Windows 7、Windows 10 等都有不同的主版本号。
Windows 版本和主版本号的关系:
Windows XP: MajorOperatingSystemVersion = 5
Windows Vista: MajorOperatingSystemVersion = 6
Windows 7: MajorOperatingSystemVersion = 6
Windows 8/8.1: MajorOperatingSystemVersion = 6
Windows 10: MajorOperatingSystemVersion = 10
Ⅱ.MinorOperatingSystemVersion
指定程序所要求的操作系统的次版本号。操作系统的次版本号通常表示某个操作系统版本的细节修订版本。例如,Windows 7 可能有多个次版本号,如 Windows 7 SP1。
Windows 版本和次版本号的关系:
Windows XP (没有 SP): MinorOperatingSystemVersion = 0
Windows XP (SP1): MinorOperatingSystemVersion = 1
Windows 7 (SP1): MinorOperatingSystemVersion = 1
Windows 10 (版本 1507): MinorOperatingSystemVersion = 0
Windows 10 (版本 1903): MinorOperatingSystemVersion = 3
兼容性: 当程序加载到操作系统时,如果操作系统检测到程序的版本要求与当前操作系统不匹配,操作系统可以弹出错误提示,或者程序根本无法启动。通过设置这些字段,程序开发者可以控制其程序是否在特定的操作系统版本上运行。这两个字段的经典取值有:
操作系统 | MajorOperatingSystemVersion | MinorOperatingSystemVersion |
---|---|---|
Windows XP | 5 | 1 |
Windows 7 | 6 | 1 |
Windows 10 | 10 | 0 |
Windows 10 | 10 | 1 |
例如,假设 MajorOperatingSystemVersion = 6
和 MinorOperatingSystemVersion = 1
,表示该程序要求至少 Windows Vista 或 Windows 7 及更高版本。如果操作系统版本低于 6.1(如 Windows XP),则该程序无法运行。此时找到样例程序中的这两个字段的值:
此时我的样例程序这两个字段的值为:00 06
和00 00
。主版本号为 6
通常表示该程序要求运行在 Windows Vista 或 更高版本的 Windows 操作系统(如 Windows 7、Windows 8、Windows 10);次版本号为 0
通常表示 没有特定的次版本要求。这意味着该程序应该能够在主版本号为 6 的任何操作系统上运行。该程序要求至少运行在 Windows Vista 或更高版本的操作系统上。如果你尝试在 Windows XP 或更早版本的操作系统上运行此程序,操作系统可能会拒绝启动该程序,或者该程序可能在运行时遇到问题。这两个字段不可随意更改:
⑨MajorImageVersion(WORD)和MinorImageVersion(WORD)
在 PE 文件(Portable Executable)格式中,MajorImageVersion
和 MinorImageVersion
是 IMAGE_OPTIONAL_HEADER
部分的两个字段,它们用于描述程序的 版本信息。
Ⅰ.MajorOperatingSystemVersion
MajorImageVersion
字段定义了程序的 主版本号。它用于标识程序的主要版本。在软件开发中,主版本号通常用于指示程序的重要变化或突破性的更新,通常与大的功能更新、改进或架构更改有关。
Ⅱ.MinorOperatingSystemVersion
inorImageVersion
字段定义了程序的 次版本号。它通常表示程序的一些较小更新、改进或者修复。与 MajorImageVersion
一起使用,MinorImageVersion
可以描述程序从一个版本到另一个版本的小范围变化。
在软件的开发和发布过程中,开发者会使用这两个字段来标识程序的版本。主版本号通常随着大的功能更新或重构而增加,而次版本号则随着小的更新或修复而变化。
假设 PE 文件中以下字段的值:
-
MajorImageVersion = 1
-
MinorImageVersion = 5
这表示该程序的版本号为 1.5
,这意味着程序处于第一个大版本,并且可能在 1.0
版本之后有了一些小的更新或修复,版本号的次版本号为 5。
示例程序中这两个字段的值:00 00
(表示该程序版本为0.0)
修改这俩字段的值,重新运行程序,发现这两个字段的修改并不影响程序运行:
⑩MajorSubsystemVersion(WORD)和MinorSubsystemVersion(WORD)
Ⅰ.MajorSubsystemVersion
MajorSubsystemVersion
字段指定程序所要求的 主子系统版本号。操作系统根据这个字段来确认程序所需的子系统版本。如果程序所要求的子系统版本较高,操作系统会确保加载并提供该子系统环境。
Ⅱ.MinorSubsystemVersion
MinorSubsystemVersion
字段指定程序所要求的 次子系统版本号。与 MajorSubsystemVersion
一起,MinorSubsystemVersion
描述了程序的完整子系统版本要求。
什么是子系统?
子系统是操作系统提供的环境,用于支持不同类型的程序。PE 文件中的子系统信息决定了程序将运行在什么样的环境中。常见的子系统类型包括:
①Windows GUI:图形用户界面应用程序,通常不使用控制台(如大多数桌面应用程序)。
②Windows CUI:控制台应用程序(如命令行程序)。
③POSIX:用于支持 POSIX 标准的子系统,通常在 Windows 上使用类似 Cygwin 的环境。
④EFI:用于支持 UEFI(统一可扩展固件接口)环境下的程序。
经检验,这两个字段不可随意修改。
⑪Win32VersionValue(DWORD)
该字段是一个DWORD类型的字段,Win32VersionValue
字段在 PE 文件格式中并不是非常常用,它的值通常被设置为 0x00000000
,但也可能存储其他值来表示特定的版本信息。这个字段主要在早期版本的 Windows 操作系统中被使用,用于标识 32 位 Windows 版本,但在大多数情况下,它并不对程序的运行产生影响。
⑫ SizeOfImage(DWORD)
SizeOfImage
是一个关键字段,它告诉操作系统或加载器在将程序加载到内存时需要为该程序分配多大的内存空间。这个大小不仅包括程序代码和数据,还包括由操作系统加载和初始化程序时需要的其他信息(如导入表、重定位表等)。
SizeOfImage
的计算通常包括以下内容:
所有节(Section)的内存占用:PE 文件通常包含多个节,如 .text、.data、.rdata 等,每个节的大小决定了映像占用的内存空间。 页对齐(Page Alignment):操作系统在加载程序时,通常会对节进行页对齐(即按系统页面大小对齐)。因此,SizeOfImage 可能会比文件的实际大小大一些,因为它包含了对齐后的内存空间。
经验证,该值不可随意修改:
此时若是将该值调大:则程序正常运行。
若此时将该字段值调小(小于原来的值):则程序无法正常运行:
⑬ SizeOfHeaders(DWORD)
SizeOfHeaders
主要用于标识文件头部部分的总大小。操作系统在加载程序时,首先会读取文件头(包括 DOS 头、PE 头、可选头和节表头等),并根据这些头部信息来判断如何加载和执行该程序。
该字段的值通常会帮助操作系统加载器确定文件头部分的结束位置,并从文件中读取其余部分(即程序的代码和数据段等)。
SizeOfHeaders
计算公式为:
SizeOfHeaders = DOS header size + PE header size + File header size + Optional header size + Section header size
SizeOfHeaders
字段表示 PE 文件头部分的总大小,包含了所有与程序加载相关的元数据。这个字段的值包括 DOS 头、PE 头、文件头、可选头和节头的大小,并帮助操作系统确定程序的加载方式。SizeOfHeaders
的大小通常是固定的,但如果节数量增加,节头部分的大小会随之变化。
经检验,该字段不可随意更改。
⑭CheckSum(DWORD)
CheckSum
字段的主要作用是验证 PE 文件的完整性。在文件传输、下载、拷贝或其他操作后,可以使用 CheckSum
来检查文件是否发生了任何错误或损坏。如果文件被修改或损坏,计算出的校验和将与原始的 CheckSum
值不同。
实际例子:
在某些情况下,操作系统或防病毒软件可能会检查 CheckSum
来确定 PE 文件是否被篡改,或者在加载文件时验证文件的完整性。
例如,在安装一个程序时,安装程序可能会计算下载的安装包的校验和并与存储在文件中的校验和进行比较。如果校验和不匹配,说明文件可能在下载过程中损坏,或者文件被篡改,安装程序会提示用户。
经检验:该字段在修改后不影响程序运行(win11中)
⑮ Subsystem(WORD)
Subsystem
字段,用于指定程序的子系统类型,子系统类型决定了该程序在操作系统中运行时所依赖的环境和特性。例如,它决定程序是作为控制台应用程序运行,还是作为图形用户界面(GUI)应用程序运行。该字段的值是一个常量,指定了程序所依赖的操作系统功能和库。
在Visual Studio中可以找到Subsystem
的相关取值:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
#define IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG 17
此时样例程序该字段的取值为:0002
,通过对照该表我们就可以清楚该程序是一个GUI程序,运行在Windows的GUI子系统。
若此时我们将样例程序该字段的取值改为:0003(Image runs in the Windows character subsystem.)
再次运行后就发现该程序会自动弹出黑窗口。最后经过验证发现,该程序不可随意修改:
⑯ DllCharacteristics(WORD)
DllCharacteristics
字段用于指示 DLL 文件(动态链接库)的一些特性和行为。它是一个位掩码字段,包含一组特性标志,描述了该 DLL 的一些重要属性,特别是在加载、链接和执行时的行为。在Visual Studio
查看NT头结构题的页面,向下拉,就可以看到对应的DllCharacteristics
字段的取值。
#define IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA 0x0020 // Image can handle a high entropy 64-bit virtual address space.
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL can move.(表示该 DLL 支持地址空间布局随机化)
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // Code Integrity Image(表示强制 DLL 完整性检查)
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 // Image is NX compatible(表示该 DLL 支持执行禁用(NX, No eXecute)功能。)
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // Image understands isolation and doesn't want it(表示该 DLL 不使用 Windows 的 DLL 隔离机制。)
#define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // Image does not use SEH. No SE handler may reside in this image(表示该 DLL 不使用结构化异常处理)
#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // Do not bind this image.
#define IMAGE_DLLCHARACTERISTICS_APPCONTAINER 0x1000 // Image should execute in an AppContainer
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // Driver uses WDM model
#define IMAGE_DLLCHARACTERISTICS_GUARD_CF 0x4000 // Image supports Control Flow Guard.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000 //表示该 DLL 是终端服务器兼容的。
此时样例程序该字段的值为0x8140
:
通过对照该表可知:表示该 DLL 支持地址空间布局随机化(0x40)、该 DLL 支持执行禁用功能(0x100)、该 DLL 是终端服务器兼容的(8000x)。
IMAGE_DLLCHARACTERISTICS_NX_COMPAT
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
最后,经过尝试,发现该字段不可随意更改。
⑰SizeOfStackReserve (DWORD)和 SizeOfStackCommit(DWORD)、SizeOfHeapReserve(DWORD) 和 SizeOfHeapCommit(DWORD)**(可以修改,但是不能随意修改)
Ⅰ.SizeOfStackReserve
该字段表示栈的预留大小,即操作系统为程序预留的栈空间的总大小。这个值是操作系统在加载程序时保留的虚拟内存空间的大小,但是这些内存页并不会立即被物理内存填充,直到程序开始使用它们。如果 SizeOfStackReserve
为 0x00100000
(1MB),则表示操作系统为该程序预留了 1MB 的虚拟内存空间,用于栈的使用。
Ⅱ.MinorSubsystemVersion
该字段表示栈的提交大小,即操作系统为程序分配并加载到物理内存中的栈空间的大小。这个值表示栈的初始物理内存分配量,在程序运行时,栈空间的初始部分会被分配给程序。如果 SizeOfStackCommit
为 0x00020000
(128KB),则表示操作系统在程序启动时会为栈分配 128KB 的物理内存。这部分内存是立即可用的,而不是仅仅在虚拟内存中预留的。
Ⅲ.SizeOfHeapReserve 和 SizeOfHeapCommit:
这两个字段分别表示为程序的堆保留和提交的内存大小。
SizeOfHeapReserve
指定了操作系统为堆保留的虚拟内存大小。此空间是程序可以使用的,但初始时并不会映射到实际的物理内存中。SizeOfHeapCommit
指定了初始为程序分配的物理内存大小,通常较小,只为程序的堆提供基础内存,只有在程序需要更多堆空间时,操作系统才会进一步分配更多内存。
操作系统通常会使用两种策略来管理堆栈的内存分配:
预留大小:一般来说,SizeOfStackReserve 的值较大,因为它表示操作系统为栈保留的地址空间。这个值通常会设置得足够大,以便程序能够使用较大的栈空间。 提交大小:SizeOfStackCommit 的值通常较小,因为它表示最初分配给程序的物理内存。操作系统会根据程序的需求,逐步扩大提交的内存区域。
⑱LoaderFlags(DWORD)
LoaderFlags
的标志值通常用来指定一些特殊的加载选项,特别是关于程序加载的方式。不过这些标志在许多实际的应用中并未被广泛使用,很多时候它的值为 0。随意修改该字段的值,并不影响程序正常运行。
⑲ NumberOfRvaAndSizes(DWORD)
该字段表示数据目录(Data Directories)数组中有效条目的数量。Data Directories
数组包含了 PE 文件中多个重要信息的 RVA 和大小。NumberOfRvaAndSizes
定义了数组中实际使用的条目数。它的值是一个小于或等于 16 的数字,因为 Data Directories
数组的最多只能有 16 个元素。
如果 NumberOfRvaAndSizes
为 10,那么数据目录中前 10 个条目是有效的,之后的条目没有被使用,可能是 0 或者无效的。如果 NumberOfRvaAndSizes
为 16,那么数据目录中的所有条目都是有效的,表示所有的 16 个数据目录项都有对应的 RVA 和大小。
此时样例程序中NumberOfRvaAndSizes
值为00 00 00 01
这个时候则表示数据目录中的只有一条数据是有效的。验证:修改该字段,并不影响程序运行。
⑳DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES](数据目录)
DataDirectory
是一个长度为 16 的数组,表示 PE 文件中可能包含的最多 16 个数据目录。虽然数组有 16 个条目,但并不是所有条目都会被使用。NumberOfRvaAndSizes
字段会指示实际使用了多少个数据目录。DataDirectory
包含指向不同类型数据的相对虚拟地址(RVA)和该数据的大小。这些数据包括导入表、导出表、资源表等,用于在程序加载和执行时进行访问,该字段的类型为_IMAGE_DATA_DIRECTORY
,在 PE 文件中,_IMAGE_DATA_DIRECTORY
是一个包含两个字段的结构体,通常定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {DWORD VirtualAddress;DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
VirtualAddress
:指向数据的相对虚拟地址(RVA),即该数据在内存中的位置。通过该地址,加载器可以找到该数据。
Size
:该数据的大小(以字节为单位)。如果该字段为 0,表示数据不存在或没有相关内容。
用途:
DataDirectory
包含了 PE 文件中的 16 个数据目录条目,每个条目描述了一段特定的数据,指明了该数据在内存中的位置及其大小。常见的数据目录条目包括:
导入表(Import Table):描述程序使用的外部函数和符号。通过这个表,程序可以访问外部库(DLL)中的函数。
导出表(Export Table):列出程序对外提供的函数和符号,其他程序可以通过这个表调用它们。
资源表(Resource Table):存储程序的各种资源数据,如图标、位图、字符串等。
重定位表(Base Relocation Table):包含程序的地址重定位信息,用于程序加载到非默认地址时调整指针和地址。
异常表(Exception Table):存储异常处理相关信息。
安全表(Security Table):存储安全相关信息,如签名。
调试信息(Debug Data):包含调试信息,如符号、源文件信息等。
TLS 表(TLS Table):线程局部存储(Thread Local Storage)表,指示程序如何访问线程本地数据。
加载配置表(Load Config Table):存储与程序加载配置相关的信息,如安全和调试配置。
Bound Import Table:用于标记程序是否已经绑定到 DLL 中的符号。
IAT(Import Address Table):存储程序导入的符号的地址。
数组中放哪个表,具体看底下的宏定义(Visual Studio
):
各个条目在数组中的位置入下:
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory资源表
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory异常
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory安全表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory调试西悉尼
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory TLS表
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers 存储程序与 DLL 文件绑定的符号信息。
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table 存储函数的实际地址。
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors 延迟导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
数据目录(DataDirectory
)在 PE 文件中包含了多个关键的数据结构和表(如导入表、导出表、重定位表等),这些数据对于程序的正常加载和运行至关重要。因此,数据目录不能随意修改,否则可能导致程序无法正确加载或运行。