PE

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 4 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個字節)

偏移
(PE32/PE32+)

大小 英文名 描述
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。

Windows子系統

爲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

DLL特徵

爲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進制字才能找到另一個證書。

證書的起始位置和長度由證書表中相應的表項給出。每個證書都有惟一一個與它對應的表項。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章