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 文件中包含了多個關鍵的數據結構和表(如導入表、導出表、重定位表等),這些數據對于程序的正常加載和運行至關重要。因此,數據目錄不能隨意修改,否則可能導致程序無法正確加載或運行。