1 基本概念
下表描述了貫穿於本文中的一些概念:
名稱 | 描述 |
地址 | 是“虛擬地址”而不是“物理地址”。為什么不是“物理地址”呢?因為數據在內存的位置經常在變,這樣可以節省內存開支、避開錯誤的內存位置等的優勢。同時用戶並不需要知道具體的“真實地址”,因為系統自己會為程序准備好內存空間的(只要內存足夠大) |
鏡像文件 | 包含以EXE文件為代表的“可執行文件”、以DLL文件為代表的“動態鏈接庫”。為什么用“鏡像”?這是因為他們常常被直接“復制”到內存,有“鏡像”的某種意思。看來西方人挺有想象力的哦^0^ |
RVA | 英文全稱Relatively Virtual Address。偏移(又稱“相對虛擬地址”)。相對鏡像基址的偏移。 |
節 | 節是PE文件中代碼或數據的基本單元。原則上講,節只分為“代碼節”和“數據節”。 |
VA | 英文全稱Virtual Address。基址 |
2 概覽
x86都是32位的,IA-64都是64位的。64位 Windows需要做的只是修改PE格式的少數幾個域。這種新的格式被稱為PE32+。它並沒有增加任何新域,僅從PE格式中刪除了一個域。其余的改變就 是簡單地把某些域從32位擴展到64位。在大部分情況下,你都能寫出同時適用於32位和64位PE文件的代碼。
EXE文件與DLL文件的區別完全是語義上的。它們使 用的是相同的PE格式。惟一的不同在於一個位,這個位用來指示文件應該作為EXE還是DLL。甚至DLL文件的擴展名也完全也是人為的。你可以給DLL一 個完全不同的擴展名,例如.OCX控件和控制面板小程序(.CPL)都是DLL。
圖1 解釋了Microsoft PE可執行文件格式:
PE文件總體上分為“頭”和“節”。“頭”是“節”的描述、簡化、說明,“節”是“頭”的具體化。
3 文件頭
PE文件的頭分為DOS頭、NT頭、節頭。注意,這是本人的分法,在此之前並沒有這種分法。這樣分法會更加合理,更易理解。因為這三個部分正好構成SizeOfHeaders所指的范圍,所以將它們合為“頭”。這里的3個頭與別的文章的頭的定義會有所區別。
節頭緊跟在NT頭后面。
3.1 DOS頭(PE文件簽名的偏移地址就是大小)
用記事本打開任何一個鏡像文件,其頭2個字節必為字符 串“MZ”,這是Mark Zbikowski的姓名縮寫,他是最初的MS-DOS設計者之一。然后是一些在MS-DOS下的一些參數,這些參數是在MS-DOS下運行該程序時要用 到的。在這些參數的末尾也就是文件的偏移0x3C(第60字節)處是是一個4字節的PE文件簽名的偏移地址。該地址有一個專用名稱叫做“E_lfanew”。這個簽名是“PE00”(字母“P”和“E”后跟着兩個空字節)。 緊跟着E_lfanew的是一個MS-DOS程序。那是一個運行於MS-DOS下的合法應用程序。當可執行文件(一般指exe、com文件)運行於MS- DOS下時,這個程序顯示“This program cannot be run in DOS mode(此程序不能在DOS模式下運行)”這條消息。用戶也可以自己更改該程序,有些還原軟件就是這么干的。同時,有些程序既能運行於DOS又能運行於 Windows下就是這個原因。Notepad.exe整個DOS頭大小為224個字節,大部分不能在DOS下運行的Win32文件都是這個值。MS- DOS程序是可有可無的,如果你想使文件大小盡可能的小可以省掉MS-DOS程序,同時把前面的參數都清0。
3.2 NT頭(244或260個字節)
緊跟着PE文件簽名之后,是NT頭。NT頭分成3個部分,因為第2部分在32與64位系統里有區別,第3部分雖然也是頭,但實際很不像“頭”。
第1部分(20個字節)
偏移 | 大小 | 英文名 | 中文名 | 描述 |
0 | 2 | Machine | 機器數 | 標識CPU的數字。參考3.2.1節“機器類型”。 |
2 | 2 | NumberOfSections | 節數 | 節的數目。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 | FillCharacteristics | 文件特征值 | 指示文件屬性的標志。參考3.2.2節“特征”。 |
第2部分(96或112個字節)
偏移 | 大小 | 英文名 | 中文名 | 描述 |
0 | 2 | Magic | 魔數 | 這個無符號整數指出了鏡像文件的狀態。 0x10B表明這是一個32位鏡像文件。 0x107表明這是一個ROM鏡像。 0x20B表明這是一個64位鏡像文件。 |
2 | 1 | MajorLinkerVersion | 鏈接器的主版本號 | 鏈接器的主版本號。 |
3 | 1 | MinorLinkerVersion | 鏈接器的次版本號 | 鏈接器的次版本號。 |
4 | 4 | SizeOfCode | 代碼節大小 | 一般放在“.text”節里。如果有多個代碼節的話,它是所有代碼節的和。必須是FileAlignment的整數倍,是在文件里的大小。 |
8 | 4 | SizeOfInitializedData | 已初始化數大小 | 一般放在“.data”節里。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在文件里的大小。 |
12 | 4 | SizeOfUninitializedData | 未初始化數大小 | 一般放在“.bss”節里。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在文件里的大小。 |
16 | 4 | AddressOfEntryPoint | 入口點 | 當可執行文件被加載進內存時其入口點RVA。對於一般程序鏡像來說,它就是啟動地址。為0則從ImageBase開始執行。對於dll文件是可選的。 |
20 | 4 | BaseOfCode | 代碼基址 | 當鏡像被加載進內存時代碼節的開頭RVA。必須是SectionAlignment的整數倍。 |
24 | 4 | BaseOfData | 數據基址 | 當鏡像被加載進內存時數據節的開頭RVA。(在64位文件中此處被並入緊隨其后的ImageBase中。)必須是SectionAlignment的整數倍。 |
28/24 | 4/8 | ImageBase | 鏡像基址 | 當加載進內存時鏡像的第1個字節的首選地址。它必須是64K的倍數。DLL默認是10000000H。Windows CE 的EXE默認是00010000H。Windows 系列的EXE默認是00400000H。 |
32 | 4 | SectionAlignment | 內存對齊 | 當加載進內存時節的對齊值(以字節計)。它必須≥FileAlignment。默認是相應系統的頁面大小。 |
36 | 4 | FileAlignment | 文件對齊 | 用來對齊鏡像文件的節中的原始數據的對齊因子(以字節計)。它應該是界於512和64K之間的2的冪(包括這兩個邊界值)。默認是512。如果SectionAlignment小於相應系統的頁面大小,那么FileAlignment必須與SectionAlignment相等。 |
40 | 2 | MajorOperatingSystemVersion | 主系統的主版本號 | 操作系統的版本號可以從“我的電腦”→“幫助”里面看到,Windows XP是5.1。5是主版本號,1是次版本號 |
42 | 2 | MinorOperatingSystemVersion | 主系統的次版本號 | |
44 | 2 | MajorImageVersion | 鏡像的主版本號 | |
46 | 2 | MinorImageVersion | 鏡像的次版本號 | |
48 | 2 | MajorSubsystemVersion | 子系統的主版本號 | |
50 | 2 | MinorSubsystemVersion | 子系統的次版本號 | |
52 | 2 | Win32VersionValue | 保留,必須為0 | |
56 | 4 | SizeOfImage | 鏡像大小 | 當鏡像被加載進內存時的大小,包括所有的文件頭。向上舍入為SectionAlignment的倍數。 |
60 | 4 | SizeOfHeaders | 頭大小 | 所有頭的總大小,向上舍入為FileAlignment的倍數。可以以此值作為PE文件第一節的文件偏移量。 |
64 | 4 | CheckSum | 校驗和 | 鏡像文件的校驗和。計算校驗和的算法被合並到了Imagehlp.DLL 中。以下程序在加載時被校驗以確定其是否合法:所有的驅動程序、任何在引導時被加載的DLL以及加載進關鍵Windows進程中的DLL。 |
68 | 2 | Subsystem | 子系統類型 | 運行此鏡像所需的子系統。參考后面的“Windows子系統”部分。 |
70 | 2 | DllCharacteristics | DLL標識 | 參考后面的“DLL特征”部分。 |
72 | 4/8 | SizeOfStackReserve | 堆棧保留大小 | 最大棧大小。CPU的堆棧。默認是1MB。 |
76/80 | 4/8 | SizeOfStackCommit | 堆棧提交大小 | 初始提交的堆棧大小。默認是4KB。 |
80/88 | 4/8 | SizeOfHeapReserve | 堆保留大小 | 最大堆大小。編譯器分配的。默認是1MB。 |
84/96 | 4/8 | SizeOfHeapCommit | 堆棧交大小 | 初始提交的局部堆空間大小。默認是4KB。 |
88/104 | 4 | LoaderFlags | 保留,必須為0 | |
92/108 | 4 | NumberOfRvaAndSizes | 目錄項數目 | 數據目錄項的個數。由於以前發行的Windows NT的原因,它只能為16。 |
第3部分數據目錄(128個字節)
偏移 |
大小 | 英文名 | 描述 |
96/112 | 8 | Export Table | 導出表的地址和大小。參考5.1節“.edata” |
104/120 | 8 | Import Table | 導入目錄表的地址和大小。參考5.2.1節“.idata” |
112/128 | 8 | Resource Table | 資源表的地址和大小。參考5.6節“.rsrc” |
120/136 | 8 | Exception Table | 異常表的地址和大小。參考5.3節“.pdata” |
128/144 | 8 | Certificate Table | 屬性證書表的地址和大小。參考6節“屬性證書表” |
136/152 | 8 | Base Relocation Table | 基址重定位表的地址和大小。參考5.4節“.reloc” |
144/160 | 8 | Debug | 調試數據起始地址和大小。 |
152/168 | 8 | Architecture | 保留,必須為0 |
160/176 | 8 | Global Ptr | 將被存儲在全局指針寄存器中的一個值的RVA。這個結構的Size域必須為0 |
168/184 | 8 | TLS Table | 線程局部存儲(TLS)表的地址和大小。 |
176/192 | 8 | Load Config Table | 加載配置表的地址和大小。參考5.5節“加載配置結構” |
184/200 | 8 | Bound Import | 綁定導入查找表的地址和大小。參考5.2.2節“導入查找表” |
192/208 | 8 | IAT | 導入地址表的地址和大小。參考5.2.4節“導入地址表” |
200/216 | 8 | Delay Import Descriptor | 延遲導入描述符的地址和大小。 |
208/224 | 8 | CLR Runtime Header | CLR運行時頭部的地址和大小。(已廢除) |
216/232 | 8 | 保留,必須為0 |
3.2.1 機器類型
Machine域可以取以下各值中的一個來指定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處理器 |
3.2.2 特征
Characteristics域包含鏡像文件屬性的標志。以下加粗的是常用的屬性。當前定義了以下值(由低位往高位):
位置 | 描述 |
0 | 它表明此文件不包含基址重定位信息,因此必須被加載到其首選基地址上。如果基地址不可用,加載器會報錯。 |
1 | 它表明此鏡像文件是合法的。看起來有點多此一舉,但又不能少。 |
2 | 保留,必須為0。 |
3 | |
4 | |
5 | 應用程序可以處理大於2GB的地址。 |
6 | 保留,必須為0。 |
7 | |
8 | 機器類型基於32位體系結構。 |
9 | 調試信息已經從此鏡像文件中移除。 |
10 | 如果此鏡像文件在可移動介質上,完全加載它並把它復制到交換文件中。幾乎不用 |
11 | 如果此鏡像文件在網絡介質上,完全加載它並把它復制到交換文件中。幾乎不用 |
12 | 此鏡像文件是系統文件,而不是用戶程序。 |
13 | 此鏡像文件是動態鏈接庫(DLL)。 |
14 | 此文件只能運行於單處理器機器上。 |
15 | 保留,必須為0。 |
為NT頭第2部分的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 |
為NT頭的DllCharacteristics域定義了以下值:
位置 | 描述 |
1 | 保留,必須為0。 |
2 | |
3 | |
4 | |
5 | 官方文檔缺失 |
6 | 官方文檔缺失 |
7 | DLL可以在加載時被重定位。 |
8 | 強制進行代碼完整性校驗。 |
9 | 鏡像兼容於NX。 |
10 | 可以隔離,但並不隔離此鏡像。 |
11 | 不使用結構化異常(SE)處理。 |
12 | 不綁定鏡像。 |
13 | 保留,必須為0。 |
14 | WDM驅動程序。 |
15 | 官方文檔缺失 |
16 | 可以用於終端服務器。 |
每個數據目錄給出了Windows使用的表或字符串的地址和大小。這些數據目錄項全部被被加載進內存以備系統運行時使用。數據目錄是按照如下格式定義的一個8字節結構:
typedef struct
DWORD VirtualAddress; //數據的RVA
DWORD Size; //數據的大小
typedef ENDS
第1個域——VirtualAddress,實際上是表的RVA。相對鏡像基址偏移地址。NT頭第2部分的ImageBase
第2個域給出了表的大小(以字節計)。數據目錄組成了NT頭的最后一部分。
Certificate Table域指向屬性證書表。它的第一個域是一個文件指針,而不是通常的RVA。
3.3 節頭
在鏡像文件中,每個節的RVA值必須由鏈接器決定。這樣能夠保證這些節位置相鄰且按升序排列,並且這些RVA值必須是NT頭中SectionAlignment域的倍數。
每個節頭(節表項)格式如下,共40個字節:
偏移 | 大小 | 英文名 | 描述 |
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 | 描述節特征的標志。參考“節標志”。 |
3.3.1 節標志
節頭中的Characteristics標志指出了節的屬性。(以下加粗的是常用的屬性值)
位置 | 描述 |
1 | 已經廢除 |
2 | |
3 | |
4 | |
5 | |
6 | 此節包含可執行代碼。代碼段才用“.text” |
7 | 此節包含已初始化的數據。“.data” |
8 | 此節包含未初始化的數據。“.bss” |
9 | 已經廢除 |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | 此節包含通過全局指針(GP)來引用的數據。 |
17 | 已經廢除 |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | 此節包含擴展的重定位信息。 |
26 | 此節可以在需要時被丟棄。 |
27 | 此節不能被緩存。 |
28 | 此節不能被交換到頁面文件中。 |
29 | 此節可以在內存中共享。 |
30 | 此節可以作為代碼執行。 |
31 | 此節可讀。(幾乎都設置此節) |
32 | 此節可寫。 |
第25標志表明節中重定位項的個數超出了節頭中為每個 節保留的16位所能表示的范圍(也就是65535個函數)。如果設置了此標志並且節頭中的NumberOfRelocations域的值是0xffff, 那么實際的重定位項個數被保存在第一個重定位項的VirtualAddress域(32位)中。如果設置了第25標志但節中的重定位項的個數少於 0xffff,則表示出現了錯誤。
4 一些注意信息
1.PE頭是怎么計算的?
SizeOfHeaders所指的頭是從文件的第1個 字節開始算起的,而不是從PE標記開始算起的。快速的計算方法是從文件的偏移0x3C(第59字節)處獲得一個4字節的PE文件簽名的偏移地址,這個偏移 地址就是本文所定義的DOS頭的大小。NT頭在32位系統是244字節,在64位系統是260字節。節頭的大小由NT頭的第1部分的 NumberOfSections(節的數量)*40字節(每個節頭是40字節)得出。如此,DOS頭、NT頭、節頭3個頭的大小加起來並向上舍入為 FileAlignment(文件對齊)的正整數倍的最小值就是SizeOfHeaders(頭大小)值。
2.節數量的問題
Windows讀取NumberOfSections 的值然后檢查節表里的每個結構,如果找到一個全0結構就結束搜索,否則一直處理完NumberOfSections指定數目的結構。沒有規定節頭必須以全 0結構結束。所以加載器使用了雙重標准——全0、達到NumberOfSections數量就不再搜索了。
3.未初始化問題
①未初始化數據在文件中是不占空間的,但在內存里還是會占空間的,它們依然依據指定的大小存在內存里。所以說未初始化數據只在文件大小上有優勢,在內存里與已初始化數據是一樣的。
②未初始化數據的方法有2種:1是通過節頭的VirtualSize>SizeOfRawData。未初始化數據的大小就是VirtualSize-SizeOfRawData的值。2是節特征的標志置為“此節包含未初始化的數據”,這時SizeOfUninitializedData才會非0。現在 都使用第1種,把它們集成到.data里面可以加快速度。
4.已初始化問題
數據目錄里面所對應的塊中除了屬性證書表、調試信息和幾個廢除的目錄項外,全都屬於SizeOfInitializedData(已初始化數據大小)范圍。當然,已初始化數據不只這些,還可以是常見的代碼段等等。
5.節對齊的問題
如果NT頭的SectionAlignment域的值小於相應操作系統(有 些資料說是根據CPU來的,這不一定。因為CPU本身就允許改分頁大小,只是大部分時候操作系統是用CPU默認值的。x86平台默認頁面大小是4K。 IA-64平台默認頁面大小是8K。MIPS平台默認頁面大小是4K。Itanium平台默認頁面大小是8K。)平台的頁面大小,那么鏡像文件有一些附加 的限制。對於這種文件,當鏡像被加載到內存中時,節中數據在文件中的位置必須與它在內存中的位置相等,因此節中數據的物理偏移與RVA相同。
6.鏡像大小
SizeOfImage所代表的內存鏡像大小沒有包含屬性證書表和調試信息,這是因為加載器並不將屬性證書和調試信息映射進內存。同時加載器規定,屬性證書和調試信息必須被放在鏡像文件的最后,並且屬性證書表在調試信息節之前。
7.數據的組織
CPU的段主要分為4個:代碼段、數據段、堆棧段、附加段。而操作系統給程序員留下只有代碼段和數據段,堆棧段和附加段就由系統自行處理了,我們不用管。PE文件的數據組織方式是以BaseOfCode、BaseOfData為基准,以節為主體,以數據目錄為輔助。
①BaseOfCode、BaseOfData是與后面相應的代碼節、數據節的VirtualAddress一致。(這里的數據節是狹義的數據節,是特指代碼段、數據目錄所指定的數據除外的那一部分,也就是我們編程時定義的常量、變量、未初始化數據等)
②所有的代碼、數據都必須在節里面,否則就算是代碼基址、數據基址、數據目錄都有指定,而節頭里沒有指定,加載器也會報錯,不能運行
③導入函數、導出函數、資源、重定位表等是為了輔助程序主體的,這些都由系統負責處理
5 特殊的節
下表描述了保留的節以及它們的屬性,后面是對出現在可執行文件中的節的詳細描述。這些節是微軟的編譯產品所定義的不是系統定義的,實際可以不拘泥於此。
節名 | 內容 |
.bss | 未初始化的數據 |
.data | 代碼節 |
.edata | 導出表 |
.idata | 導入表 |
.idlsym | 包含已注冊的SEH,它們用以支持IDL屬性 |
.pdata | 異常信息 |
.rdata | 只讀的已初始化數據(用於常量) |
.reloc | 重定位信息 |
.rsrc | 資源目錄 |
.sbss | 與GP相關的未初始化數據 |
.sdata | 與GP相關的已初始化數據 |
.srdata | 與GP相關的只讀數據 |
.text | 默認代碼節 |
5.1 .edata節
文件A的函數K被文件B調用時,函數K就稱為導出函數。導出函數通常出現在DLL中,也可以是exe文件。
下表描述了導出節的一般結構。
表名 | 描述 |
導出目錄表 | 它給出了其它各種導出表的位置和大小。 |
導出地址表 | 一個由導出函數的RVA組成的數組。它們是導出的函數和數據在代碼節和數據節內的實際地址。其它鏡像文件可以通過使用這個表的索引(序數)來調用函數。 |
導出名稱指針表 | 一個由指向導出函數名稱的指針組成的數組,按升序排列。大小寫敏感。 |
導出序數表 | 一個由對應於導出名稱指針表中各個成員的序數組成的數組。它們的對應是通過位置來體現的,因此導出名稱指針表與導出序數表成員數目必須相同。 |
導出名稱表 | 一系列以NULL結尾的ASCII碼字符串。導出名稱指針表中的成員都指向這個區域。它們都是公用名稱,函數導入與導出就是通過它們。 |
當其它鏡像文件通過名稱導入函數時,Win32加載器 通過導出名稱指針表來搜索匹配的字符串。如果找到,它就查找導出序數表中相應的成員(也就是說,將找到的導出名稱指針表的索引作為導出序數表的索引來使 用)來獲取與導入函數相關聯的序數。獲取的這個序數是導出地址表的索引,這個索引對應的元素給出了所需函數的實際位置。每個導出函數都可以通過序數進行訪 問。
當其它鏡像文件通過序數導入函數時,就不再需要通過導出名稱指針表來搜索匹配的字符串。因此直接使用序數效率會更高。但是導出名稱容易記憶,它不需要用戶記住各個符號在表中的索引。
5.1.1 導出目錄表
導出目錄表是導出函數信息的開始部分,它描述了導出函數信息中其余部分的內容。
偏移 | 大小 | 英文名 | 描述 |
0 | 4 | Export Flags | 保留,必須為0。 |
4 | 4 | Time/Date StampMajor Version | 導出函數被創建的日期和時間。這個值與NT頭的第一部分TimeDateStamp相同。 |
8 | 2 | Major Version | 主版本號。 |
10 | 2 | Minor Version | 次版本號。 |
12 | 4 | Name RVA | 包含這個DLL全名的ASCII碼字符串RVA。以一個NULL字節結尾。 |
16 | 4 | Ordinal Base | 導出函數的起始序數值。它通常被設置為1。 |
20 | 4 | NumberOfFunctions | 導出函數中所有元素的數目。 |
24 | 4 | NumberOfNames | 導出名稱指針表中元素的數目。它同時也是導出序數表中元素的數目。 |
28 | 4 | AddressOfFunctions | 導出地址表RVA。 |
32 | 4 | AddressOfNames | 導出名稱指針表RVA。 |
36 | 4 | AddressOfNameOrdinals | 導出序數表RVA。 |
5.1.2 導出地址表(Export Address Table,EAT)
導出地址表的格式為下表所述的兩種格式之一。如果指定的地址不是位於導出節(其地址和長度由NT頭給出)中,那么這個域就是一個Export RVA;否則這個域是一個Forwarder RVA,它給出了一個位於其它DLL中的符號的名稱。
偏移 | 大小 | 域 | 描述 |
0 | 4 | Export RVA | 當加載進內存時,導出函數RVA。 |
0 | 4 | Forwarder RVA | 這是指向導出節中一個以NULL結尾的ASCII碼字符串的指針。這個字符串必須位於Export Table(導出表)數據目錄項給出的范圍之內。這個字符串給出了導出函數所在DLL的名稱以及導出函數的名稱(例如“MYDLL.expfunc”), 或者DLL的名稱以及導出函數的序數值(例如“MYDLL.#27”)。 |
Forwarder RVA導出了其它鏡像中定義的函數,使它看起來好像是當前鏡像導出的一樣。因此對於當前鏡像來說,這個符號同時既是導入函數又是導出函數。
例如對於Windows XP系統中的Kernel32.dll文件來說,它導出的“HeapAlloc”被轉發到“NTDLL.RtlAllocateHeap”。這樣就允許應 用程序使用Windows XP系統中的Ntdll.dll模塊而不需要實際包含任何相關的導入信息。應用程序的導入表只與Kernel32.dll有關。
導出地址表的的值有時為0,此時表明這里沒有導出函數。這是為了能與以前版本兼容,省去修改的麻煩。
5.1.3 導出名稱指針表
導出名稱指針表是由導出名稱表中的字符串的地址(RVA)組成的數組。二進制進行排序的,以便於搜索。
只有當導出名稱指針表中包含指向某個導出名稱的指針時,這個導出名稱才算被定義。換句話說,導出名稱指針表的值有可能為0,這是為了能與前面版本兼容。
5.1.4 導出序數表
導出序數表是由導出地址表的索引組成的一個數組,每個序數長16位。必須從序數值中減去Ordinal Base域的值得到的才是導出地址表真正的索引。注意,導出地址表真正的索引真正的索引是從0開始的。由此可見,微軟弄出Ordinal Base是找麻煩的。導出序數表的值和導出地址表的索引的值都是無符號數。
導出名稱指針表和導出名稱序數表是兩個並列的數組,將 它們分開是為了使它們可以分別按照各自的邊界(前者是4個字節,后者是2個字節)對齊。在進行操作時,由導出名稱指針這一列給出導出函數的名稱,而由導出 序數這一列給出這個導出函數對應的序數。導出名稱指針表的成員和導出序數表的成員通過同一個索引相關聯。
5.1.5 導出名稱表(Export Name Table,ENT)
導出名稱表的結構就是長度可變的一系列以NULL結尾的ASCII碼字符串。 導出名稱表包含的是導出名稱指針表實際指向的字符串。這個表的RVA是由導出名稱指針表的第1個值來確定的。這個表中的字符串都是函數名稱,其它文件可以通過它們調用函。
5.1.6 舉例
①用序數調用
當可執行文件用序數調用函數時,該序數就是導出函數地址表的真實索引。如果索引是錯誤的就有可能出現不可預知的錯誤。最著名的例子就是Windows XP在升級Server 2補丁之后,有很多程序都不能運行就是這個原因。微軟用序數這種方法被大多數危險程序(病毒、木馬)所引用,同樣的微軟自己也用這種方法來使用一些隱含的 函數。最后受害者還是廣大的用戶,因為使用序數方法的絕大部分程序是有着不可告人的目的的。
②用函數名調用
當可執行文件用函數名調用時,加載器會通過AddressOfNames以2進制的方法找到第一個相同的函數名。假如找到的是第X個函數名,則在AddressOfNameOrdinals中取出第X個值,該值再減去Ordinal Base則為函數地址的真實索引。
5.2.idata節
首先,您得了解什么是導入函數。一個導入函數是被某模 塊調用的但又不在調用者模塊中的函數,因而命名為“import(導入)”。導入函數實際位於一個或者更多的DLL里。調用者模塊里只保留一些函數信息, 包括函數名及其駐留的DLL名。現在,我們怎樣才能找到PE文件中保存的信息呢? 轉到 data directory 尋求答案吧。
文件中導入信息的典型布局如下:
典型的導入節布局
5.2.1 導入目錄表
導入目錄表是由導入目錄項組成的數組,每個導入目錄項對應着一個導入的DLL。最后一個導入目錄項是空的(全部域的值都為NULL),用來指明目錄表的結尾。
每個導入目錄項的格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Import Lookup Table RVA | 導入查找表的RVA。這個表包含了每一個導入函數的名稱或序數。 |
4 | 4 | Time/Date Stamp | 當鏡像與相應的DLL綁定之后,這個域被設置為這個DLL的日期/時間戳。 |
8 | 4 | Forwarder Chain | 第一個轉發項的索引。 |
12 | 4 | Name RVA | 包含DLL名稱的ASCII碼字符串RVA。 |
16 | 4 | Import Address RVA | 導入地址表的RVA。這個表的內容與導入查找表的內容完全一樣。 |
5.2.2 導入查找表
導入查找表是由長度為32位(PE32)或64位 (PE32+)的數字組成的數組。其中的每一個元素都是位域,其格式如下表所示。在這種格式中,位31(PE32)或位63(PE32+)是最高位。這些 項描述了從給定的DLL導入的所有函數。最后一個項被設置為0(NULL),用來指明表的結尾。
偏移 | 大小 | 位域 | 描述 |
31/63 | 1 | Ordinal/Name Flag | 如果這個位為1,說明是通過序數導入的。否則是通過名稱導入的。測試這個位的掩碼為0x80000000(PE32)或)0x8000000000000000(PE32+)。 |
15-0 | 16 | Ordinal Number | 序數值(16位長)。只有當Ordinal/Name Flag域為1(即通過序數導入)時才使用這個域。位30-15(PE32)或62-15(PE32+)必須為0。 |
30-0 | 31 | Hint/Name Table RVA | 提示/名稱表項的RVA(31位長)。只有當Ordinal/Name Flag域為0(即通過名稱導入)時才使用這個域。對於PE32+來說,位62-31必須為0。 |
5.2.3 提示/名稱表
提示/名稱表中的每一個元素結構如下:
偏移 | 大小 | 域 | 描述 |
0 | 2 | Hint | 指出名稱指針表的索引。當搜索匹配字符串時首選使用這個值。如果匹配失敗,再在DLL的導出名稱指針表中進行2進制搜索。 |
2 | 可變 | Name | 包含導入函數名稱的ASCII碼字符串。這個字符串必須與DLL導出的函數名稱匹配。同時這個字符串區分大小寫並且以NULL結尾。 |
* | 0或1 | Pad | 為了讓提示/名稱表的下一個元素出現在偶數地址,這里可能需要填充0個或1個NULL字節。 |
5.2.4 導入地址表
導入地址表的結構和內容與導入查找表完全一樣,直到文件被綁定。在綁定過程中,用導入函數的32位(PE32)或64位(PE32+)地址覆蓋導入地址表中的相應項。這些地址是導入函數的實際內存地址,盡管技術上仍把它們稱為“虛擬地址”。加載器通常會處理綁定。
5.3 .pdata節(可有可無,誰也不希望自己的函數出問題的吧!)
.pdata節是由用於異常處理的函數表項組成的數組。NT頭中的Exception Table(異常表)域指向它。在將它們放進最終的鏡像文件之前,這些項必須按函數地址(下列每個結構的第一個域)排序。下面描述了函數表項的3種格式,使用哪一種取決於目標平台。
對於32位的MIPS鏡像來說,其函數表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函數的VA |
4 | 4 | End Address | 函數結尾的VA |
8 | 4 | Exception Handler | 指向要執行的異常處理程序的指針 |
12 | 4 | Handler Data | 指向要傳遞給異常處理程序的附加數據的指針 |
16 | 4 | Prolog End Address | 函數prolog代碼結尾的VA |
對於ARM、PowerPC、SH3和SH4 Windows CE平台來說,其函數表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函數的VA |
4 | 8位 | Prolog Length | 函數prolog代碼包含的指令數 |
4 | 22位 | Function Length | 函數代碼包含的指令數 |
4 | 1位 | 32-bit Flag | 如果此位為1,表明函數由32位指令組成。否則,函數由16位指令組成。 |
4 | 1位 | Exception Flag | 如果此位為1,表明存在用於此函數的異常處理程序;否則,不存在異常處理程序。 |
對於x64和Itanium平台來說,其函數表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函數的RVA |
4 | 4 | End Address | 函數結尾的RVA |
8 | 4 | Unwind Information | 用於異常處理的展開(Unwind)信息的RVA |
5.4 .reloc節
基址重定位表包含了鏡像中所有需要重定位的內容。NT 頭中的數據目錄中的Base Relocation Table(基址重定位表)域給出了基址重定位表所占的字節數。基址重定位表被划分成許多塊,每一塊表示一個4K頁面范圍內的基址重定位信息,它必須從 32位邊界開始。
5.4.1 基址重定位塊
每個基址重定位塊的開頭都是如下結構:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Page RVA | 將鏡像基址與這個域(頁面RVA)的和加到每個偏移地址處最終形成一個VA,這個VA就是要進行基址重定位的地方。 |
4 | 4 | Block Size | 基址重定位塊所占的總字節數,其中包括Page RVA域和Block Size域以及跟在它們后面的Type/Offset域。 |
Block Size域后面跟着數目不定的Type/Offset位域。它們中的每一個都是一個WORD(2字節),其結構如下:
偏移 | 大小 | 域 | 描述 |
0 | 4位 | Type | 它占這個WORD的最高4位,這個值指出需要應用的基址重定位類型。參考5.4.2節“基址重定位類型”。 |
0 | 12位 | Offset | 它占這個WORD的其余12位,這個值是從基址重定位塊的Page RVA域指定的地址處開始的偏移。這個偏移指出需要進行基址重定位的位置。 |
為了進行基址重定位,需要計算鏡像的首選基地址與實際被加載到的基地址之差。如果鏡像本身就被加載到了其首選基地址,那么這個差為零,因此也就不需要進行基址重定位了。
5.4.2 基址重定位類型
值 | 描述 |
0 | 基址重定位被忽略。這種類型可以用來對其它塊進行填充。 |
1 | 基址重定位時將差值的高16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的高半部分。 |
2 | 基址重定位時將差值的低16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的低半部分。 |
3 | 基址重定位時將所有的32位差值加到指定偏移處的一個32位域上。 |
4 | 進行基址重定位時將差值的高16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的高半部分,而這個32位字的低半部分被存儲在緊 跟在這個Type/Offset位域后面的一個16位字中。也就是說,這一個基址重定位項占了兩個Type/Offset位域的位置。 |
5 | 對MIPS平台的跳轉指令進行基址重定位。 |
6 | 保留,必須為0 |
7 | 保留,必須為0 |
9 | 對MIPS16平台的跳轉指令進行基址重定位。 |
10 | 進行基址重定位時將差值加到指定偏移處的一。 |
5.5 加載配置結構(不清楚,大概又是多余的吧)
加載配置結構最初用於Windows NT操作系統自身幾種非常有限的場合——在鏡像文件頭或NT頭中描述各種特性太困難或這些信息尺寸太大。當前版本的Microsoft鏈接器和 Windows XP以及后續版本的Windows使用的是這個結構的新版本,將之用於包含保留的SEH技術的基於x86的32位系統上。它提供了一個安全的結構化異常處 理程序列表,操作系統在進行異常派送時要用到這些異常處理程序。如果異常處理程序的地址在鏡像的VA范圍之內,並且鏡像被標記為支持保留的SEH,那么這 個異常處理程序必須在鏡像的已知安全異常處理程序列表中,否則操作系統將終止這個應用程序。這是為了防止利用“x86異常處理程序劫持”來控制操作系統, 它在以前已經被利用過。
Microsoft的鏈接器自動提供一個默認的加載配置結構來包含保留的SEH數據。如果用戶的代碼已經提供了一個加載配置結構,那么它必須包含新添加的保留的SEH域。否則,鏈接器將不能包含保留的SEH數據,這樣鏡像文件就不能被標記為包含保留的SEH。
5.5.1 加載配置目錄
對應於預保留的SEH加載配置結構的數據目錄項必須為 加載配置結構指定一個特別的大小,因為操作系統加載器總是希望它為這樣一個特定值。事實上,這個大小只是用於檢查這個結構的版本。為了與Windows XP以及以前版本的Windows兼容,x86鏡像文件中這個結構的大小必須為64。
5.5.2 加載配置結構布局
用於32位和64位PE文件的加載配置結構布局如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Characteristics | 指示文件屬性的標志,當前未用。 |
4 | 4 | TimeDateStamp | 日期/時間戳。這個值表示從UTC時間1970年1月1日午夜(00:00:00)以來經過的總秒數,它是根據系統時鍾算出的。可以用C運行時函數time來獲取這個時間戳。 |
8 | 2 | MajorVersion | 主版本號 |
10 | 2 | MinorVersion | 次版本號 |
12 | 4 | GlobalFlagsClear | 當加載器啟動進程時,需要被清除的全局加載器標志。 |
16 | 4 | GlobalFlagsSet | 當加載器啟動進程時,需要被設置的全局加載器標志。 |
20 | 4 | CriticalSectionDefaultTimeout | 用於這個進程處於無約束狀態的臨界區的默認超時值。 |
24 | 8 | DeCommitFreeBlockThreshold | 返回到系統之前必須釋放的內存數量(以字節計)。 |
32 | 8 | DeCommitTotalFreeThreshold | 空閑內存總量(以字節計)。 |
40 | 8 | LockPrefixTable | [僅適用於x86平台]這是一個地址列表的VA。這個地址列表中保存的是使用LOCK前綴的指令的地址,這樣便於在單處理器機器上將這些LOCK前綴替換為NOP指令。 |
48 | 8 | MaximumAllocationSize | 最大的分配粒度(以字節計)。 |
56 | 8 | VirtualMemoryThreshold | 最大的虛擬內存大小(以字節計)。 |
64 | 8 | ProcessAffinityMask | 將這個域設置為非零值等效於在進程啟動時將這個設定的值作為參數去調用SetProcessAffinityMask函數(僅適用於.exe文件)。 |
72 | 4 | ProcessHeapFlags | 進程堆的標志,相當於函數的第一個參數。這些標志用於在進程啟動過程中創建的堆。 |
76 | 2 | CSDVersion | Service Pack版本標識。 |
78 | 2 | Reserved | 必須為0 |
80 | 8 | EditList | 保留,供系統使用。 |
60/88 | 4/8 | SecurityCookie | 指向cookie的指針。cookie由Visual C++編譯器的GS實現所使用。 |
64/96 | 4/8 | SEHandlerTable | [僅適用於x86平台]這是一個地址列表的VA。這個地址列表中保存的是鏡像中每個合法的、獨一無二的SE處理程序的RVA,並且它們已經按RVA排序。 |
68/104 | 4/8 | SEHandlerCount | [僅適用於x86平台]表中獨一無二的SE處理程序的數目。 |
5.6 .rsrc節
資源節可以看成是一個磁盤的分區,盤符是資源目錄表,下面有3層目錄(資源目錄項),最后是文件(資源數據)。
①資源目錄表是一個16字節組成的結構。其第一個字節又稱為“根節點”。其前的12字節雖然有定義,但加載器並不理會,所以任何值都可以。
②第1層目錄(資源目錄項)是資源類型,微軟已經定義了21種。其結構是一個16字節的數組。資源目錄項分為名稱項和ID項,這取決於資源目錄表。資源目錄表指出跟着它的名稱項和ID項各有多少個(表中所有的名稱項在所有的ID項前面)。表中的所有項按升序排列:名稱項是按不區分大小寫的字符串,而ID項則是按數值。第0-3字節表示資源類型的名稱字符串的地址或是32位整數,第4-7字節表示第二層目錄(資源目錄項)相對於根節點的偏移。
一系列資源目錄表按如下方式與各層相聯系:每個目錄表 后面跟着一系列目錄項,它們給出那個層(類型、名稱或語言)的名稱或標識(ID)及其數據描述或另一個目錄表的地址。如果這個地址指向一個數據描述,那么 那個數據就是這棵樹的葉子。如果這個地址指向另一個目錄表,那么那個目錄表列出了下一層的目錄項。
一個葉子的類型、名稱和語言ID由從目錄表到這個葉子的路徑決定。第1個表決定類型ID,第2個表(由第一個表中的目錄項指向)決定名稱ID,第3個表決定語言ID。
.rsrc節的一般結構如下:
數據 | 描述 |
資源目錄表 | 所有的頂層(類型)結點都被列於第1個表中。這個表中的項指向第2層表。每個第2層樹的類型ID相同但是名稱ID不同。第3層樹的類型ID和名稱ID都相同但語言ID不同。每個單個的表后面緊跟着目錄項,每一項都有一個名稱或數字標識和一個指向數據描述或下一層表的指針。 |
資源目錄項 | |
資源目錄字符串 | 按2字節邊界對齊的Unicode字符串,它是作為由資源目錄項指向的字符串數據來使用的。 |
資源數據描述 | 一個由記錄組成的數組,由表指向它,描述了資源數據的實際大小和位置。這些記錄是資源描述樹中的葉子。 |
資源數據 | 資源節的原始數據。資源據描述域中的大小和位置信息將資源數據分成單個的區域。 |
資源目錄表
偏移 | 大小 | 域 | 描述 |
0 | 4 | Characteristics | 資源標志。保留供將來使用。當前它被設置為0。 |
4 | 4 | Time/Date Stamp | 資源數據被資源編譯器創建的時間。 |
8 | 2 | Major Version | 主版本號,由用戶設定。 |
10 | 2 | Minor Version | 次版本號,由用戶設定。 |
12 | 2 | Number of Name Entries | 緊跟着這個表頭的目錄項的個數,這些目錄項使用名稱字符串來標識類型、名稱或語言項。 |
14 | 2 | Number of ID Entries | 緊跟着這個表頭的目錄項的個數,這些目錄項使用數字來標識類型、名稱或語言項。 |
資源目錄項
具體的情況是資源目錄表后面緊跟着以名稱項和ID項所組成的數組。資源目錄表與資源目錄項之間不能有空隙。名稱項組成的數組在ID項組成的數組前面,且兩個數組不能有空隙。
偏移 | 大小 | 域 | 描述 |
0 | 4 | Name RVA | 表示類型、名稱或語言ID項的名稱字符串的地址。 |
0 | 4 | Integer ID | 表示類型、名稱或語言ID項的32位整數。 |
4 | 4 | Data Entry RVA | 最高位為0。低31位是資源數據項的地址。 |
4 | 4 | Subdirectory RVA | 最高位為1。低31位是另一個資源目錄表(下一層)的地址。 |
資源目錄字符串
資源目錄字符串區由按字邊界對齊的Unicode字符串組成。這些字符串被存儲在最后一個資源目錄項之后、第一個資源數據項之前。這樣能夠使這些長度可變的字符串對長度固定的目錄項的對齊情況影響最小。每個資源目錄字符串格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 2 | Length | 字符串的長度,不包括Length域本身。 |
2 | 可變 | Unicode String | 可變 Unicode String 按字邊界對齊的可變長度的Unicode字符串。 |
資源數據項
每個資源數據項描述了資源數據區中一個實際單元的原始數據。資源數據項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Data RVA | 資源數據區中一個單元的資源數據的地址。 |
4 | 4 | Size | 由Data RVA域指向的資源數據的大小(以字節計)。 |
8 | 4 | Codepage | 用於解碼資源數據中的代碼點值的代碼頁。通常這個代碼頁應該是Unicode代碼頁。 |
12 | 4 | 保留,必須為0 | 保留,必須為0 |
6 屬性證書表
可以給鏡像文件添加屬性證書表使它與屬性證書相關聯。有多種不同類型的屬性證書,最常用的是Authenticode簽名。
屬性證書表包含一個或多個長度固定的表項,可以通過 NT頭中的數據目錄中的Certificate Table(證書表)域找到它們。這個表的每個表項給出了相應證書的開始位置和長度。存儲在這個節中的每個證書都有一個相應的證書表項。證書表項的數目可 以通過將證書表的大小除以證書表中每一項的大小(8)得到。注意證書表的大小僅包括它的表項,並不包括這些表項實際指向的證書。
每個表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Certificate Data | 指向證書實際數據的文件指針。它指向的地址總是按8字節倍數邊界(即最低3個位都是0)對齊。 |
0 | 4 | Size of Certificate | 這是一個無符號整數,它指出證書的大小(以字節計)。 |
注意證書總是從8進制字(從任意字節邊界開始的16個 連續字節)邊界開始。如果一個證書的長度不是8進制字長度的偶數倍,那么就一直用0填充到下一個八進制字邊界。但是證書長度並不包括這些填充的0。因此任 何處理證書的軟件必須向上舍入到下一個8進制字才能找到另一個證書。
證書的起始位置和長度由證書表中相應的表項給出。每個證書都有惟一一個與它對應的表項。
來源:看雪論壇http://bbs.pediy.com/showthread.php?t=145912