本文主要來自相關係列博文(breaksoftware的csdn博客 https://blog.csdn.net/breaksoftware/article/details/7710323)。
自己,稍微調整,整理。
MS 2.0節是PE文件格式中第一個“節”。其大致結構如下:
在VC\PlatformSDK\Include\WinNT.h文件中有對MS-DOS 2.0兼容EXE文件頭的完整定義
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
這個結構佔用0x40個字節,其中我們將主要關注兩個成員變量:e_magic和e_lfanew。
以我xp電腦上notepad爲例,我們使用UE打開C:\windows\notepad.exe
可以發現IMAGE_DOS_HEADER結構中e_magic對應的數據位0x5A4D(MZ),e_lfanew對應的是0x000000E0。這個兩個數據是這個結構體中最需要關心的兩個成員變量。幻數(Magic Num)這個概念是用於區分一個格式文件的類型,就像一個人的姓,知道你姓啥之後,就可以明確你是不是我們族人。同樣,解析這些文件的程序也會去嘗試讀取這樣的幻數,以確認這個文件符合它要求的。在我所知道的一些格式中,他們的幻數往往是這個格式發明者的名稱縮寫(或者是格式後綴)。我們這個MS-Dos 2.0兼容EXE文件頭中的幻數MZ也是紀念他的發明者,可以想到,這個名字應該不是蓋茨,因爲MZ和Bill Gates(BG)一點也沒關係,也不是Paul Allen(PA),更不可能是銷售出生的Steve Ballmer。它是Mark Zbikowski,中文翻譯是馬克·茨柏克沃斯基。
那麼爲什麼PE格式文件會有個Dos文件頭呢?Dos系統時代,有兩種(我所知道的,我壓根沒經歷過那個年代)可執行文件格式,一種是.exe爲後綴的文件,其結構是MZ格式。另一種是以.com爲後綴的文件,其結構是COM格式。從Wiki上對MZ格式的介紹可以看出來,MZ格式要比COM格式要新,MZ格式頭中包含了重定向信息(本文第一個圖中),且其支持可執行體大於64KiB。如今我們電腦上PE可執行文件的後綴也是.exe,爲了讓該後綴程序在Dos和Nt間有個過渡,我們需要讓Dos系統能知道它不能“正確”執行該Exe文件。於是我們PE可執行文件一開始處便插入了一個MS-Dos 2.0兼容Exe文件頭,Dos系統加載我們PE文件時,從一開始讀取我們文件,發現是“DOS下可執行程序”,於是成功且順利的執行我們的程序中DOS系統可執行部分,這部分DOS程序輸出“該程序不能在DOS上”執行的提示。
現在我們來看下MS-2.0節結構圖和我們結構體的對應關係:
MS-Dos 2.0兼容Exe文件頭 對應於IMAGE_DOS_HEADER中e_magic到e_ovno
未使用 對應於 e_res[4],雖說這段沒使用,但是我還是覺得這段很有意思的。我在做註冊表沙箱時,研究了下某公司的沙箱,可是它的沙箱不讓regedit.exe進入沙箱運行,於是我就改了e_res[4]這段數據中部分,從而讓修改後的regedit.exe在它的沙箱中運行。爲什麼呢?很容易想象,“MD5+簽名”是安全公司一大“安全準繩”。我改了這個沒啥用的數據段,不會影響程序運行,但是會使MD5不同,且簽名被破壞。這段地址是(文件起始偏移0x1C)
OEM標誌 對應於 e_oemid
OEM信息 對應於 e_oeminfo
OEM信息和PE文件頭偏移 之間存在一段空白,這段空白對應於 e_res2[10],這段數據和之前e_res[4]一樣,改改也無妨。這段地址是(偏移0x28)
PE文件頭偏移 對應於 e_lfanew,其位於0x3C偏移處。
MS-Dos 2.0佔位程序和重定向表和未使用數據段如下圖,因爲我也沒仔細研究過這個結構,所以也不能準確區分出哪塊是佔位程序,哪塊是重定向表,哪塊是未使用段。
從上面的數據我們可以看到,如果我們程序運行在Dos下,會輸出“This program connot be run in Dos mode"。
那麼NT系統加載我們的PE可執行程序呢?它不會去執行DOS佔位程序,而會跳到PE頭位置繼續讀取和執行。PE頭位置就是e_lfanew字段的值,該值是PE頭和文件頭的之間的偏移量。如本例中就是0x000000E0。我們去該偏移去查看數據
看到PE了麼?這個PE是PE頭的Magic Num。我會在之後介紹PE文件頭及其相關知識。
簡短總結,以我自己手上的 notepad++.exe 這個文件爲例,如下圖:
第一個紅色框內就是上面文章講的主要內容,最後四個字節是 0x00000120。如箭頭所指向的地址。以5045開頭。這一部分以及之後的是稍後的內容。
==============================================================================
首先說一下VC中對應的數據結構。“簽名、COFF文件頭和可選文件頭”這三部分信息組合在一起是一個叫IMAGE_NT_HEADERS的結構體。
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中Signature對應於“簽名”,FileHeader對應於“COFF文件頭”,OptionalHeader對應於“可選文件頭”。
對於PE鏡像文件,Signature對應的數據是0x00004550(‘PE\0\0’)。對於如何找到這個位置,在前一篇文章中已經有了解說:從文件頭偏移0x3C讀取一個DWORD大小的數據,從文件頭偏移該數據長度,就到了Signature的起始位置。
看一下COFF文件頭結構
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;
以notepad爲例
Signature 的值就是 0x00004550。
IMAGE_FILE_HEADER 有7個字段,圖中用紅色框標註。
Machine字段爲0x014C,其對應的信息是“Intel 386或其後續處理器及兼容處理器”。
NumberOfSections是0x0003,它是個非常重要的字段,表示節的數目。PE文件是由一系列“節”構成的,比較常見的是.text和.data等節,這樣的獨立的區塊是用來存儲“代碼”、“數據”和“資源”等信息的。如xp上notepad,從數據中我們可以看到它有3個節,我們用其他工具分析得到它確實存在如下3個節。
TimeDateStamp是0x41107CC3,該字段記錄的是文件創建時間離1970年1月1日00:00的秒數。
(可以參考工具網站進行時間轉換:http://tool.chinaz.com/Tools/unixtime.aspx)
PointerToSymbolTable是0x00000000,該字段記錄了該PE文件中調試信息符號表。由於符號表信息是在程序運行時不需要加載進入內存的,所以這個偏移使用的是相對文件頭偏移RA。目前微軟推薦是:將映像文件調試符號表信息獨立的放在PDB文件中,所以不會在PE文件中再保存調試符號表信息,於是這個字段應該爲0。當然這並不是硬性要求,我發現我電腦上就存在很多該字段不爲0的文件。剛開始時我也不是很明白它們爲什麼要使用這個字段,特別是其指向的字符表個數(NumberOfSymbols)爲0!!你說既然大小爲0,那你指向有什麼意思呢?其實這種設計是非常有深意的,我會在之後的章節中介紹這種深意。
NumberOfSymbols是0x00000000,該字段記錄了該PE文件中調試信息符號表元素個數。對於映像文件,該字段爲0(非硬性要求),,理由在PointerToSymbolTable中已經說明。通過NumberOfSymbols和PointerToSymbolTable,我們可以找到字符串表起始位置,因爲字符串表緊跟在符號表之後。
SizeOfOptionalHeader是0x00E0,該字段用於描述“可選文件頭”的大小。之後會看到“可選文件頭”的中有個具有16個元素是數組,該數組保存了一系列“塊信息”,但是並不是所有文件都有全部的“塊信息”,於是鏈接器在鏈接生成PE文件時,也是根據實際存在的“塊信息”位置(以後會說明爲什麼是位置而不是數量)去填充這個數組的。也就是說我們可能只是填充了1個元素,而剩下的15個元素直接被砍掉,而不是在內存中使用0來填充。
這兒就引入一個問題,就是我們不能從“簽名”位置開始,就直接memcpy一段IMAGE_NT_HEADERS大小的空間到一個IMAGE_NT_HEADERS對象中。因爲“可選文件頭”還要看“COFF文件頭”中的SizeOfOptionalHeader數據。
Characteristics字段用於標記該文件屬性,notepad.exe該字段值爲0x010F。下面我們來解釋下該組合屬性。
標誌 | 值 | 說明 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 僅適用於映像文件。它表明此文件不包含機制重定位信息,於是它只能被加載到其首選基地址。如果首選基地址不可用,則加載器會報錯。鏈接器默認會移除可執行文件中的重定位信息。一般情況下,Exe文件會設置該值(如notepad.exe,但ntoskrnl.exe就沒設置),而因爲DLL文件爲了其良好的兼容性是不會去設置這個值的(如Kernel32.dll、User32.dll等)。 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 僅適用於映像文件。它用於表明該文件是合法的,可以被運行。如果沒有設置,則代表鏈接出現問題。這個一般都會設置。 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | COFF行號信息已經被移除。不贊成使用該標誌。但是我發現notepad.exe、Kernel32.dll、User32.dll等都設置了該標誌。而一般我們編譯的PE文件是不設置該項的。 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | COFF符號表中有關局部符號的項已經被移除。不贊成使用該標誌。但是我發現notepad.exe、Kernel32.dll、User32.dll等都設置了該標誌。而一般我們編譯的PE文件是不設置該項的。 |
IMAGE_FILE_AGGRESSIVE_WS_TRIM | 0x0010 | 該標誌已經被廢棄。 |
IMAGE_FILE_LARGE_ADDRESS_ AWARE | 0x0020 | 應用程序可以處理大於2GB的地址。 |
0x0040 | 爲未來保留的字段。 | |
IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 小尾,LSB在MSB前面。不贊成使用該標誌。windows xp就是小尾。 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 適用於32位系統。我的xp系統上DLL和Exe文件基本都設置了該標誌。 |
IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 調試信息已經從該映像文件中移除。 |
IMAGE_FILE_REMOVABLE_RUN_ FROM_SWAP | 0x0400 | 如果該文件是在移動介質上,需要將其完全加載到交換文件中。 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 如果該文件是在網絡介質上,需要將其完全加載到交換文件中。 |
IMAGE_FILE_SYSTEM | 0x1000 | 該映像文件是一個系統文件,不是一個用戶文件。 |
IMAGE_FILE_DLL | 0x2000 | 此文件是DLL文件。 |
IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 該文件僅能運行於單處理機器上。 |
IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 大尾,LSB在MSB後面。 |
我觀察了我係統上幾個文件,發現以下規律:
1 Sys和Exe的該屬性爲0x010E或者0x010F。
2 DLL文件該屬性一般爲0x210E。DLL文件一般不會設IMAGE_FILE_RELOCS_STRIPPED(0x0001),因爲它爲了良好的兼容性,不能設置它必須要被加載的地址。一個Exe可能會加載多個DLL,如果系統“不小心”把某個DLL加載到0x70000000,那麼如果有某個DLL設置了IMAGE_FILE_RELOCS_STRIPPED並將其首選加載地址正好也設置爲0x70000000,那麼系統爲該Exe加載這個DLL將會失敗。但是的確存在這樣的文件,比如我電腦上ResourceCache.dll。DLL文件肯定要設置IMAGE_FILE_DLL。所以即使某個DLL文件的後綴名改了,你可以結合這個“特徵碼”來還原其真面目。
這兒我還要說一個認知的誤區。 IMAGE_FILE_32BIT_MACHINE標誌可以用於標誌這個文件是適用於32位系統,但是如果僅僅通過該標誌就去鑑別這個文件是32位文件還是64位文件是不正確的。我也不知道微軟爲什麼設計了該標誌而沒有嚴格限制這個標誌。我通過掃描我電腦裏所有文件,發現了一個可能具有指導性的鑑別策略:
1 如果沒有設置 IMAGE_FILE_32BIT_MACHINE但是設置了IMAGE_FILE_LARGE_ADDRESS_ AWARE的文件是64位文件。沒有設置IMAGE_FILE_32BIT_MACHINE意味着該文件可能是64位程序,而設置了IMAGE_FILE_LARGE_ADDRESS_ AWARE,則說明該文件可以處理大於2G的空間的內存,則該文件是64位文件。如我本機上wwst64.exe。
2 除了以上判斷之外的其他可能標誌該文件是32位文件。
比如設置了IMAGE_FILE_32BIT_MACHINE而沒有設置IMAGE_FILE_LARGE_ADDRESS_ AWARE,則說明這個文件可以處理2G以內內存空間,是32位文件;
比如沒有設置IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,怎麼解釋呢?反正它不是64位文件,因爲不能處理大於2G內存空間,那它只能是32位文件了。如我本機上文件sqlite3.dll。
比如設置了IMAGE_FILE_32BIT_MACHINE和IMAGE_FILE_LARGE_ADDRESS_ AWARE,那說明這是個可以處理大於2G內存空間的32位文件。如我本機上AcroBroker.exe。
接下來是 IMAGE_OPTIONAL_HEADER32 和 IMAGE_OPTIONAL_HEADER64
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
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;
64位版本和32位版本很類似:沒有BaseOfCode屬性;ImageBase、SizeofStackReserve、SizeOfStackCommit、SizeOfHeapReserve和SizeOfHeapCommit等5個屬性由32位版的DWORD改成ULONGLONG。看下詳細的64位版定義
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG 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;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
可見32 位的結構是 96 字節 + IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
可見64 位的結構是 112 字節 + IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
我們觀察這個32位版本結構體,可以看到該結構體包含兩塊數據:Standard fields和NT additional fields。我們可以猜想到,該結構體應該在第一個NT操作系統之前就存在了,只是當時其內容只有Standard fields(以後稱爲標準域)下的內容,後來NT系統增加了NT additional fields(以後稱爲擴展域)下元素。
此處需要特別注意一點,我們看兩個在WinNT.h中定義的結構體
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
該結構給出了PE文件頭的結構體佈局,但是切記,這僅僅是佈局。我們千萬不要想當然的認爲直接從PE頭部開始將IMAGE_NT_HEADERS32(64)結構體大小的數據拷貝到該結構體對象中。
memcpy( &ImageNTHeader32,lpPEStart,sizeof(IMAGE_NT_HEADERS32 );// 這是錯誤的!!
爲什麼?因爲一個文件中不一定有完整的IMAGE_OPTIONAL_HEADER32(64)結構體對象信息。原因在前面做了介紹,IMAGE_FILE_HEADER中字段SizeOfOptionalHeader指定了該文件中保存的“可選文件頭”真實長度,我們應該根據該元素來給IMAGE_OPTIONAL_HEADER32(64)對象賦值。
我們的文件是使用IMAGE_OPTIONAL_HEADER32還是IMAGE_OPTIONAL_HEADER64結構體呢?可能有人會記起,我們在前面介紹了判斷文件是32位還是64位的方法,我們是否可以通過該判斷的結果來判斷是哪種結構體呢?最開始我也是這麼想的,後來我發現我電腦上 Microsoft Visual Studio 10.0\VC\lib\amd64\Microsoft.VisualC.STLCLR.dll 文件是個64位文件但是使用了IMAGE_OPTIONAL_HEADER32 結構體!!!是不是很驚訝!我不知道微軟這麼設計的原因,但是我知道了通過之前判斷是否爲64位文件來決定可選文件頭結構體類型是錯誤的。那如何判斷呢?
其實是有標記的。緊跟着 IMAGE_FILE_HEADER 結構體的肯定是 IMAGE_OPTIONAL_HEADER32(64)的Magic字段。如果該字段是0x010B,則是使用了IMAGE_OPTIONAL_HEADER32(稱爲PE32);如果是0x020B,則使用了IMAGE_OPTIONAL_HEADER32(稱爲PE32+)。切記 PE32 和 PE32+ 和這個文件是32位文件還是64位文件是沒有關係的!它們是兩種不同的概念!切記要分清。
以下是兩張圖示例,也可用這兩張圖結合上面的內容再分析回顧以下
PE32 0x0B:
PE32+ 020B:
現在我們將重心放到IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];這個數組元素,我在《可選文件頭1》中對此有了點描述,而且我還說可選文件頭大小要看這個數組元素的“位置”(而不是個數)來決定的。現在我來細說下。先看下微軟的聲明
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
#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
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#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保存了指向“塊信息”的目錄信息,其中包括偏移(除了IMAGE_DIRECTORY_ENTRY_SECURITY元素是相對文件偏移RA,其他都是相對虛擬首地址偏移RVA)和大小。如果某文件只包含IMAGE_DIRECTORY_ENTRY_EXPORT(0) 、IMAGE_DIRECTORY_ENTRY_IMPORT(1) 和IMAGE_DIRECTORY_ENTRY_BASERELOC(5)等三個目錄,則IMAGE_DIRECTORY_ENTRY_EXCEPTION(2)、IMAGE_DIRECTORY_ENTRY_SECURITY(3)和IMAGE_DIRECTORY_ENTRY_SECURITY(4)的信息都要被填充0。於是IMAGE_FILE_HEADER::SizeOfOptionalHeader所指定的可選文件頭大小爲DataDirectory之前的元素總大小加上6(最後一個目錄IMAGE_DIRECTORY_ENTRY_BASERELOC所在的位置5+1)*sizeof(IMAGE_DATA_DIRECTORY)。這就說明了爲什麼可選文件頭大小是根據目錄的位置而不是數量來決定的。
再回頭看這個 PE32+ 的內容。根據 IMAGE_FILE_HEADER 結構中定義 SizeOfOptionalHeader 值是 0x00F0,前面說到 IMAGE_OPTIONAL_HEADER64 是 112 字節+ IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]。0x00F0 是240。240 - 112 = 128 字節。而一個IMAGE_DATA_DIRECTORY 是八個字節。128 = 8 X 8。也就是這裏只有數組長度是8,而不是
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
同理上面 PE32圖中可以看到 SizeOfOptionalHeader 的值是0x00E0, 0x00E0 - 96 = 224 - 96 = 128。
也就是他們的 DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] 長度是一樣的,都只有八個。
================================================================================
下篇博文我們將詳細說一下IMAGE_OPTIONAL_HEADER32和IMAGE_OPTIONAL_HEADER64中其他元素的意義。
下面這部分字段含義尚未特別驗證,先記錄下來
Magic字段是可選文件頭幻數,0x10b是32位版,0x20b是64位版。注意該屬性不能說明這個文件是64位文件還是32位文件,至於判斷是多少位文件的方案我在《PE2》中已經有了說明。
MajorLinkerVersion和MinorLinkerVersion分別對應於鏈接器的版本號,比如我電腦上VS2005編譯的文件的這兩個版本號是8.0;VS2008編譯的是9.0;VS2010編譯的是10.0。
MajorOperatingSystemVersion和MinorOperatingSystemVersion是所需要的最低的系統版本號的主版本號和次版本號。我看了下我電腦上文件,基本上是4.0。
MajorImageVersion和MinorImageVersion是映像文件的主版本號和次版本號。注意:我們在資源中定義的文件版本號不是通過這兩個屬性來體現的!目前我也沒找到在VC工程設置中可以設置這兩個屬性的地方。
Subsystem是該文件運行於的子系統信息。一般我們在windows平臺上遇到的是2,它對應於IMAGE_SUBSYSTEM_WINDOWS_GUI。
MajorSubsystemVersion和MinorSubsystemVersion是子系統的版本號。熟悉windows的朋友應該知道,微軟剛開始設計系統時,是設計成一個平臺性質——可以運行3個子系統(OS/2、POSIX和Windows)的系統。這個就是這兩個屬性的由來。
SectionAlignment是當映像文件加載到內存中時節的對齊值,該大小使用字節來衡量的。它必須要大於我們之後介紹的FileAlignment。它的默認值是相應系統的頁面大小。
FileAlignment 是映像文件節中的對齊值,它也是用字節來衡量的。英文文檔中說該字段的值要在2^9 ~ 216之間,我掃描了下我的系統,發現我係統中文件並不是如此,特別是sys文件,它們的FileAlignment小於29(512)。
SizeOfCode是文件中代碼段的總共大小。要注意一點,這個大小和.text的大小不一定一致,因爲有些代碼可能還保存在其他節中。如我電腦上AliAppLoader.exe文件,其SizeOfCode大小是0x1D600,而.text節大小隻有0x1D400,另外的0x200是在.orpc這個節中。
SizeOfInitializedData是文件中所有已經初始化數據節的大小。和SizeOfCode一樣,初始化數據不一定只在一個節中。
SizeOfUninitializedData是文件中所有未初始化數據節的大小。和SizeOfCode一樣,未初始化數據不一定只在一個節中。
Win32VersionValue是保留字段,應該爲0。那麼目前這個字段就是程序不關心的了,我們可以利用這個位置保存一些私密信息。
SizeOfImage的官方說明是該映像文件被加載入內存時的大小,理論上它應該是SectionAlignment的倍數。但是實際並非如此,我發現我電腦上很多文件的該字段不是SectionAlignment的倍數,而有時SizeOfImage是該文件在磁盤上的大小。可以見得這個不是一個關鍵字段。
SizeOfHeaders的官方解釋是MS-DOS佔位程序、PE文件頭和節頭的總大小,且其應該是FileAlignment的倍數。但是實際上,我發現我電腦上很多文件的該字段並非FileAlignment的倍數。
CheckSum字段是映像文件的校驗和。其計算算法保存在imagehlp.dll中,導出函數名爲CheckSumMappedFile。我發現我電腦上很多文件的該PE字段和計算出來的不等。官方解釋說當驅動程序、在引導時被加載的Dll以及加載到關鍵windows進程中的DLL都需要校驗該字段以確認其合法性。
SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve和SizeOfHeapCommit分別對應於保留的棧大小、提交的棧大小、保留的堆大小和提交的堆大小。
LoaderFlags字段是保留字段,應該爲0,當然你可以不把它設爲0。
NumberOfRvaAndSizes是用來指明DataDirectory元素的個數。這兒我們要說一下,我們在IMAGE_FILE_HEADER::SizeOfOptionalHeader得到了可選文件頭的大小,而影響可選文件頭大小的就是DataDirectory元素的個數(NumberOfRvaAndSizes),那麼IMAGE_FILE_HEADER::SizeOfOptionalHeader和NumberOfRvaAndSizes之間應該存在着一種換算關係。
DllCharacteristics是屬性字段,我們看個官方說明
Constant |
Value |
Description |
|
0x0001 |
Reserved, must be zero. |
|
0x0002 |
Reserved, must be zero. |
|
0x0004 |
Reserved, must be zero. |
|
0x0008 |
Reserved, must be zero. |
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE |
0x0040 |
DLL can be relocated at load time. |
IMAGE_DLL_CHARACTERISTICS_FORCE_INTEGRITY |
0x0080 |
Code Integrity checks are enforced. |
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT |
0x0100 |
Image is NX compatible. |
IMAGE_DLLCHARACTERISTICS_ NO_ISOLATION |
0x0200 |
Isolation aware, but do not isolate the image. |
IMAGE_DLLCHARACTERISTICS_ NO_SEH |
0x0400 |
Does not use structured exception (SE) handling. No SE handler may be called in this image. |
IMAGE_DLLCHARACTERISTICS_ NO_BIND |
0x0800 |
Do not bind the image. |
|
0x1000 |
Reserved, must be zero. |
IMAGE_DLLCHARACTERISTICS_ WDM_DRIVER |
0x2000 |
A WDM driver. |
IMAGE_DLLCHARACTERISTICS_ TERMINAL_SERVER_AWARE |
0x8000 |
Terminal Server aware. |
MAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE是說Dll可以在加載時被重定向,我發現我電腦上文件SDKDBLib.dll是特例,它沒有設置這個屬性,這個文件也沒有設置IMAGE_DLLCHARACTERISTICS_ NO_SEH,即該文件不使用SEH。
以上部分內容未特別驗證,在此用一個例子分析一下看各數據是多少:
前面說到 010B 是PE32 ,結構如下,長度是 96字節 + 數組,根據上圖將數據用註釋卸載後面
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; // 0x010B
BYTE MajorLinkerVersion; // 0x0E
BYTE MinorLinkerVersion; // 0x00
DWORD SizeOfCode; // 0x0017b400
DWORD SizeOfInitializedData; // 0x00147e00
DWORD SizeOfUninitializedData; // 0x00000000
DWORD AddressOfEntryPoint; // 0x0010dc5b
DWORD BaseOfCode; // 0x00001000
DWORD BaseOfData; // 0x0017d000
//
// NT additional fields.
//
DWORD ImageBase; // 0x00400000
DWORD SectionAlignment; // 0x00000100
DWORD FileAlignment; // 0x00000200
WORD MajorOperatingSystemVersion; // 0x0005
WORD MinorOperatingSystemVersion; // 0x0001
WORD MajorImageVersion; // 0x0001
WORD MinorImageVersion; // 0x0000
WORD MajorSubsystemVersion; // 0x0005
WORD MinorSubsystemVersion; // 0x0001
DWORD Win32VersionValue; // 0x00000000
DWORD SizeOfImage; // 0x002C8000
DWORD SizeOfHeaders; // 0x00000400
DWORD CheckSum; // 0x002BC111
WORD Subsystem; // 0x0002
WORD DllCharacteristics; // 0x8140
DWORD SizeOfStackReserve; // 0x00100000
DWORD SizeOfStackCommit; // 0x00000100
DWORD SizeOfHeapReserve; // 0x00010000
DWORD SizeOfHeapCommit; // 0x00000100
DWORD LoaderFlags; // 0x00000000
DWORD NumberOfRvaAndSizes; // 0x00000010
//說明下面這個數組長度是16,佔用 16 X 8 = 128 字節。和前面分析吻合
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
===========================================================================
接上面內容之後就是一些節信息,以下圖爲例有7個節,每個節40字節(至於爲什麼是 7 和40稍後再講)。分佈如下:
下面說爲什麼是 7 和 40
再回顧前面說的定義
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
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;
可以看到 NumberOfSections 的位置是在 IMAGE_NT_HEADERS32(64)結構的第6.7字節(從0開始)。也就是 0x0007。這就是7的來歷。
再看下保存節信息的結構體IMAGE_SECTION_HEADER ,和上圖對照看就容易理解了。
#define IMAGE_SIZEOF_SHORT_NAME 8
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;
可以看到上面結構就是40個字節。
Name是使用UTF-8編碼,以\0結尾,大小爲8Byte的字符串。它是節的名稱,如上圖中的.text、.data和.rsrc。看到Name的長度爲8,你是不是在想到:小於,等於和大於?現在我們就討論下如果節名長度小於、等於和大於8的情況。
節名長度小於8 的情況。這個場景最簡單了,不足的位用\0填充。
節名長度等於8的情況。因爲結構大小是固定的,所以我們不可能找到一個空餘的位置放置\0,那麼這8byte就全部填充名字了。
以下是我收集的節名信息
IMAGESETCTIONNAME g_ImageSectionName[] = {
{'.','b','s','s','\0','\0','\0','\0'},
{'.','c','o','r','m','e','t','a'},
{'.','d','a','t','a','\0','\0','\0'},
{'.','d','e','b','u','g','$','F'},
{'.','d','e','b','u','g','$','P'},
{'.','d','e','b','u','g','$','S'},
{'.','d','e','b','u','g','$','T'},
{'.','d','r','e','c','t','v','e'},
{'.','e','d','a','t','a','\0','\0'},
{'.','i','d','a','t','a','\0','\0'},
{'.','i','d','l','s','y','m','\0'},
{'.','p','d','a','t','a','\0','\0'},
{'.','r','d','a','t','a','\0','\0'},
{'.','r','e','l','o','c','\0','\0'},
{'.','r','s','r','c','\0','\0','\0'},
{'.','s','b','s','s','\0','\0','\0'},
{'.','s','d','a','t','a','\0','\0'},
{'.','s','r','d','a','t','a','\0'},
{'.','s','x','d','a','t','a','\0'},
{'.','t','e','x','t','\0','\0','\0'},
{'.','t','l','s','\0','\0','\0','\0'},
{'.','t','l','s','$','\0','\0','\0'},
{'.','v','s','d','a','t','a','\0'},
{'.','x','d','a','t','a','\0','\0'}
};
像.debug$F這樣的就是佔用了8個byte的
節名長度大於8的情況。這個場景怎麼辦?結構體大小固定,我們不能越界寫!那我們只能在其他地方去寫了,然後在這個位置保存我們寫入數據的偏移即可!是的,PE規範就是採用的這樣的思想,只是稍微有點不同:以/開始,其後跟着一個表示偏移量的十進制數字字符串,如/4(0x2f 0 0x34 0x00 0x00 0x00 0x00 0x00 0x00)。這個數字是相對字符串表起始位置的偏移RA,我們的真實的節名就保存在字符串表中。我在我電腦上找到一個這樣的文件avcodec-52.dll。我們先看Stud_PE的分析結果
可以看到Stud_PE對第5節的名字的解析是錯的的,那正確的是什麼?現在我們要回顧《PE文件和COFF文件格式分析——簽名、COFF文件頭和可選文件頭1》,該文中我埋了一個伏筆,我把段提出來
PointerToSymbolTable是0x00000000,該字段記錄了該PE文件中調試信息符號表。由於符號表信息是在程序運行時不需要加載進入內存的,所以這個偏移使用的是相對文件頭偏移RA。目前微軟推薦是:將映像文件調試符號表信息獨立的放在PDB文件中,所以不會在PE文件中再保存調試符號表信息,於是這個字段應該爲0。當然這並不是硬性要求,我發現我電腦上就存在很多該字段不爲0的文件。剛開始時我也不是很明白它們爲什麼要使用這個字段,特別是其指向的字符表個數(NumberOfSymbols)爲0!!你說既然大小爲0,那你指向有什麼意思呢?其實這種設計是非常有深意的,我會在之後的章節中介紹這種深意。
NumberOfSymbols是0x00000000,該字段記錄了該PE文件中調試信息符號表元素個數。對於映像文件,該字段爲0(非硬性要求),,理由在PointerToSymbolTable中已經說明。通過NumberOfSymbols和PointerToSymbolTable,我們可以找到字符串表起始位置,因爲字符串表緊跟在符號表之後。
看了這段後,我想你應該對那個伏筆有了解答。想想也挺有意思,微軟不推薦在文件中包含調試信息,於是PointerToSymbolTable和NumberOfSymbols就是應該廢棄的。可是這兩個數據卻關聯着字符串表。字符串表大部分時候可以不使用,但是如果DLL中存在超過8byte的節名時又不得不用,於是只好讓PointerToSymbolTable指向字符串表開始,而NumberOfSymbols爲0。
現在我們來看下上面那個Stud_PE分析出錯的文件的文件頭信息
我們去0x001c1600+4的位置去尋找該節名字,該節名位.eh_frame,長度是9byte。
==================================================================================
以下是我的測試結果相關圖:
/4應該就是偏移。四個字節
0x00A97600 的來歷:
===============================================================================
這兒要特別說明一點,可執行文件的節名長度是不會超過8的。即使obj文件中節名存在超過8的,也會在鏈接進入可執行文件時被截斷。
VirtualSize屬性是節加載進入內存後,節在內存中的大小。如果它比SizeOfRawData大,則多餘的部分是用0x00填充的。這個性質非常重要,它是關係到RVA和RA之間換算的一個基礎。
VirtualAddress屬性是節加載進入內存後其第一個字節相對於映像基址的偏移(RVA)。
SizeOfRawData是磁盤映像文件中該節的已初始化數據的大小。對於可執行文件來說,它必須是IMAGE_OPTIONAL_HEADER32(64)::FileAlignment的倍數。.如果該節中僅包含未初始化的數據,則該字段爲0。
PointerToRawData是磁盤映像文件中該節相對於映像基址的偏移(RA)。對於可執行文件來說,它的值要是IMAGE_OPTIONAL_HEADER32(64)::FileAlignment的倍數。如果該節中僅包含未初始化的數據,則該字段爲0。
PointerToRelocations指向節中重定位項開頭的相對映像基址的偏移(RA)。可執行文件或者不能重定向的文件該字段應該爲0。
PointerToLinenumbers指向節中行號項的相對映像基址偏移(RA)。因爲已經不推薦在PE文件中包含調試信息,所以該字段一般爲0。
NumberOfRelocations是節中重定位項的個數。可執行文件和不可以重定位的文件該字段爲0。
NumberOfLinenumbers是節中行號項的個數。因爲已不推薦PE文件中包含調試信息,所以該字段一般爲0。
Characteristics描述節的特徵。
Flag |
Value |
Description |
|
0x00000000 |
Reserved for future use. |
|
0x00000001 |
Reserved for future use. |
|
0x00000002 |
Reserved for future use. |
|
0x00000004 |
Reserved for future use. |
IMAGE_SCN_TYPE_NO_PAD |
0x00000008 |
The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. |
|
0x00000010 |
Reserved for future use. |
IMAGE_SCN_CNT_CODE |
0x00000020 |
The section contains executable code. |
IMAGE_SCN_CNT_INITIALIZED_DATA |
0x00000040 |
The section contains initialized data. |
IMAGE_SCN_CNT_UNINITIALIZED_ DATA |
0x00000080 |
The section contains uninitialized data. |
IMAGE_SCN_LNK_OTHER |
0x00000100 |
Reserved for future use. |
IMAGE_SCN_LNK_INFO |
0x00000200 |
The section contains comments or other information. The .drectve section has this type. This is valid for object files only. |
|
0x00000400 |
Reserved for future use. |
IMAGE_SCN_LNK_REMOVE |
0x00000800 |
The section will not become part of the image. This is valid only for object files. |
IMAGE_SCN_LNK_COMDAT |
0x00001000 |
The section contains COMDAT data. For more information, see section 5.5.6, “COMDAT Sections (Object Only).” This is valid only for object files. |
IMAGE_SCN_GPREL |
0x00008000 |
The section contains data referenced through the global pointer (GP). |
IMAGE_SCN_MEM_PURGEABLE |
0x00020000 |
Reserved for future use. |
IMAGE_SCN_MEM_16BIT |
0x00020000 |
For ARM machine types, the section contains Thumb code. Reserved for future use with other machine types. |
IMAGE_SCN_MEM_LOCKED |
0x00040000 |
Reserved for future use. |
IMAGE_SCN_MEM_PRELOAD |
0x00080000 |
Reserved for future use. |
IMAGE_SCN_ALIGN_1BYTES |
0x00100000 |
Align data on a 1-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_2BYTES |
0x00200000 |
Align data on a 2-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_4BYTES |
0x00300000 |
Align data on a 4-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_8BYTES |
0x00400000 |
Align data on an 8-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_16BYTES |
0x00500000 |
Align data on a 16-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_32BYTES |
0x00600000 |
Align data on a 32-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_64BYTES |
0x00700000 |
Align data on a 64-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_128BYTES |
0x00800000 |
Align data on a 128-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_256BYTES |
0x00900000 |
Align data on a 256-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_512BYTES |
0x00A00000 |
Align data on a 512-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_1024BYTES |
0x00B00000 |
Align data on a 1024-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_2048BYTES |
0x00C00000 |
Align data on a 2048-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_4096BYTES |
0x00D00000 |
Align data on a 4096-byte boundary. Valid only for object files. |
IMAGE_SCN_ALIGN_8192BYTES |
0x00E00000 |
Align data on an 8192-byte boundary. Valid only for object files. |
IMAGE_SCN_LNK_NRELOC_OVFL |
0x01000000 |
The section contains extended relocations. |
IMAGE_SCN_MEM_DISCARDABLE |
0x02000000 |
The section can be discarded as needed. |
IMAGE_SCN_MEM_NOT_CACHED |
0x04000000 |
The section cannot be cached. |
IMAGE_SCN_MEM_NOT_PAGED |
0x08000000 |
The section is not pageable. |
IMAGE_SCN_MEM_SHARED |
0x10000000 |
The section can be shared in memory. |
IMAGE_SCN_MEM_EXECUTE |
0x20000000 |
The section can be executed as code. |
IMAGE_SCN_MEM_READ |
0x40000000 |
The section can be read. |
IMAGE_SCN_MEM_WRITE |
0x80000000 |
The section can be written to. |
IMAGE_SCN_LNK_NRELOC_OVFL 標誌表明節中重定位項的個數超出了節頭中爲每個節保留的16 位所能表示的範圍。如果設置了此標誌並且節頭中的NumberOfRelocations 域的值是0xffff,那麼實際的重定位項個數被保存在第一個重定位項的VirtualAddress 域(32 位)中。如果設置了IMAGE_SCN_LNK_NRELOC_OVFL
標誌但節中的重定位項的個數少於0xffff,則表示出現了錯誤。