PE文件格式分析
PE 的意思是 Portable Executable(可移植的執行體)。它是 Win32環境自身所帶的執行文件格式。它的一些特性繼承自Unix的Coff(common object file format)文件格式。“Portable Executable”(可移植的執行體)意味着此文件格式是跨Win32平台的;即使Windows運行在非Intel的CPU上,任何win32平台的PE裝載器都能識別和使用該文件格式。
PE文件在文件系統中,與存貯在磁盤上的其它文件一樣,都是二進制數據,對於操作系統來講,可以認為是特定信息的一個載體,如果要讓計算機系統執行某程序,則程序文件的載體必須符合某種特定的格式。要分析特定信息載體的格式,要求分析人員有數據分析、編碼分析的能力。在Win32系統中,PE文件可以認為.exe、.dll、.sys 、.scr類型的文件,這些文件在磁盤上存貯的格式都是有一定規律的。
PE文件格式總攬
Microsoft 引入了PE文件格式,也就是大家都熟悉的PE格式,是Win32規范的一部分。然而,PE文件來源於更早的基於VAX/VMS的公共對象文件格式(COFF)。由於最初的Windows NT小組成員很多都來自數字設備公司(DEC),於是很自然的這些開發者使用已存在的代碼以加速新的Windows NT平台的開發。
使用術語“可移植可執行”的目的是為了在所有Windows平台和所有支持的CPU上都有一個統一的文件格式。Windows NT及其以后版本,Windows 95及其以后版本和Windows CE都使用了這個相同的格式,所以說在很大程度上, 這個目的達到了。
Microsoft編譯器生成的OBJ文件使用COFF格式。通過觀察COFF格式的一些域你能知道它有多么老了,那些域使用八進制編碼!COFF OBJ 文件中有許多和PE文件一樣的數據結構和枚舉,隨后我將提到它們中的一些。
對於64位的Windows, PE格式只是進行了很少的修改。這種新的格式被叫做PE32+。沒有加入新的域,只有一個域被去除。剩下的改變只是一些域從32位擴展到了64位。在這種情況下,你能寫出和32位與64位PE文件都能一起工作的代碼。對於C++代碼,Windows頭文件的能力使這些改變很不明顯。
EXE和DLL文件之間的不同完全是語義上的。它們都使用完全相同的PE格式。僅有的區別是用了一個單個的位來指出這個文件應該被作為EXE還是一個DLL。甚至DLL文件的擴展名也是不固定的,一些具有完全不同的擴展名的文件也是DLL,比如.OCX控件和控制面板程序(.CPL文件)。
PE文件一個方便的特點是磁盤上的數據結構和加載到內存中的數據結構是相同的。加載一個可執行文件到內存中 (例如,通過調用LoadLibrary)主要就是映射一個PE文件中的幾個確定的區域到地址空間中。因此,一個數據結構比如IMAGE_NT_HEADERS (稍后我將會解釋)在磁盤上和在內存中是一樣的。關鍵的一點是如果你知道怎么在一個PE文件中找到一些東西,當這個PE文件被加載到內存中后你幾乎能找到相同的信息。
要注意到PE文件並不僅僅是被映射到內存中作為一個內存映射文件。代替的,Windows加載器分析這個PE文件並決定映射這個文件的哪些部分。當映射到內存中時文件中偏移位置較高的數據映射到較高的內存地址處。一個項目在磁盤文件中的偏移也許不同於它被加載到內存中時的偏移。然而,所有被表現出來的信息都允許你進行從磁盤文件偏移到內存偏移的轉換 (參見圖1)。
圖 1 偏移
通過Windows加載器加載PE文件到內存后,內存中的版本被稱作一個模塊。文件被映射到的起始地址稱為HMODULE。有一點值得記住:得到一個HMODULE, 你就知道那個地址處有些什么數據結構,並且你能找到內存中其它所有的數據結構。這是個很有用的功能,能被用做一些其它目的例如攔截API(Windows CE下HMODULE和加載地址並不相同,這些以后再講)。
內存中的模塊描繪一個進程所需要的可執行文件的所有代碼,數據,和資源。PE文件另一些部分只被讀取,但不會被映射 (例如重定位信息)。一些部分根本就不被映射,例如,文件末尾的調試信息。PE頭中的一個域可以告訴系統映射一個可執行文件到內存中需要多少內存。不被映射的數據放在文件末尾,這些數據之前的部分將會被映射。
描述PE格式(以及COFF文件)的主要地方是在WINNT.H文件中。在這個頭文件中,你可以找到要和PE文件一起工作所必須的每個結構定義,枚舉,和#define定義。當然,其它地方也有相關文檔。例如,MSDN中有“Microsoft Portable Executable and Common Object File Format Specification” 這篇文章。但WINNT.H 文件最終決定了PE文件的格式。
有很多檢查PE文件的工具。在它們之中有包含於Visual Studio中的Dumpbin,和包含於Platform SDK的Depends。我比較喜歡Depends因為它有一個檢查一個文件的導入表和導出表的簡潔的方式。Smidgeonsoft(http://www.smidgeonsoft.com)的PEBrowse專業版是一個很優秀的免費的PE觀察器。這篇文章中包括的PEDUMP程序功能也很全面,實現了幾乎Dumpbin的所有功能。
從API的角度來說,Microsoft的IMAGEHLP.DLL 提供了讀取和編輯PE文件的機制。
在我開始討論PE文件的詳細內容之前,讓我們首先回顧幾個基本概念,這些概念貫穿於整個PE文件格式。下面,我將討論PE文件的節,相對虛擬地址(RVAs),數據目錄,和導入函數的方法。
PE文件的節
PE文件節包含了代碼或某種數據。代碼就是程序中的可執行代碼,而數據卻有很多種。除了可讀寫的程序數據(例如全局變量)之外,節中的其它類型的數據包括導入和導出表,資源,和重定位表。每個節在內存中都有它自己的屬性,包括這個節是否含有代碼,它是只讀的還是可寫的,這個節中的數據是否可在多個進程之間共享。
一般而言,一個節中所有的代碼和數據都通過一些方法邏輯地聯系起來。一個PE文件中通常至少有兩個節:一個代碼節,一個數據節。一般地,在一個PE文件中至少有一個其它類型的數據節。在這篇文章的第二部分我將討論這幾種節。
每個節都有一個不同的名字。這個名字被用來意指節的作用。例如,一個叫做.rdata的節表示一個只讀數據節。使用節名只是為了人們方便,對操作系統來說沒有任何意義。一個命名為FOOBAR的節和一個命名為.text.的節一樣有效。Microsoft通常以一個句點作為節名的前綴,但這不是必需的。多年來,Borland鏈接器就一直使用像CODE和DATA.這樣的節名。
編譯器有一組它們生成的標准的節,對於它們沒有什么不可思議的東西。你可以創建並命名你自己的節,鏈接器很樂意在可執行文件中包括它們。在Visual C++中,你可以讓編譯器把代碼或數據放到通過#pragma 語句命名的節中。例如,下面這條語句
#pragma data_seg( "MY_DATA" )
它會使Visual C++把它生成的所有數據放到一個命名為MY_DATA的節中,而不是缺省的.data節。大多數程序都使用編譯器產生的默認節,但偶爾你也許會有把代碼或數據放到一個單獨的節中的需求。
節並不是全部由鏈接器生成的,它們其實存在於OBJ文件中,通常由編譯器把它們放到那兒。鏈接器的工作是合並OBJ文件中所有必須的節並且最終放到PE文件相應節中。例如,你的工程中的每個OBJ文件都至少有一個包含代碼的.text節。鏈接器合並這些OBJ文件中的.text節到一個PE文件中的單個的.text節中。同樣地,這些OBJ文件中的叫做.data的節被合並到PE文件中一個單個的.data節中。.LIB文件中的代碼和數據通常也被包含在可執行文件中,但那個主題已經超出本文的范圍了。
鏈接器遵循一整套規則來決定哪些節該被合並以及如何合並。OBJ文件中的某個節也許是提供給鏈接器使用的,並不會放到最終的可執行文件中去。像這樣的節是由編譯器用來以傳遞信息給鏈接器。
節有兩種對齊值,一個是在磁盤文件中的偏移另一個是在內存中的偏移。PE文件頭指定了這兩個對齊值,它們可以是不同的。每個節起始於那個對齊值的倍數的位置。例如,在PE文件中,典型的對齊值是0x200。因此,每個節開始於一個0x200的倍數的文件偏移處。
一旦加載到內存中,節總是起始於至少一個頁邊界。就是說,當一個PE節被映射到內存中后,每個節的第一個字節都符合一個內存頁。對於x86 CPUs,頁是4KB,而IA-64,頁是8KB。下面顯示了PEDUMP輸出的Windows XP KERNEL32.DLL 的.text節和.data節的一小部分。
節表
01 .text VirtSize: 00074658 VirtAddr: 00001000 raw data offs: 00000400 raw data size: 00074800...
02 .data VirtSize: 000028CA VirtAddr: 00076000 raw data offs: 00074C00 raw data size: 00002400
.text節在PE文件中的偏移為0x400,而在內存中位於KERNEL32加載地址之上第0x1000個字節處。同樣的,.data節在PE文件中的偏移為0x74C00,而在內存中位於KERNEL32加載地址之上第0x76000個字節處。
創建一個節在文件中的偏移和在內存中的偏移相同的PE文件是可能的。這會使可執行文件變得很大,但在Windows 9x或Windows Me.下可以提高加載速度。缺省的/OPT:WIN98 鏈接器選項(Visual Studio 6.0引入)可以以這種方式創建PE文件。在Visual Studio® .NET中,也許會或者也許不會使用/OPT:NOWIN98,這依賴於文件是否足夠小。
鏈接器的一個有趣的特點是可以合並節。如果兩個節有類似的,兼容的特性,它們通常可以在鏈接時被合並到一個節中。這可通過/merge 選項做到。例如,下面的鏈接器選項合並.rdata和.text節到一個單個的命名為.text的節中。
/MERGE:.rdata=.text
合並節的好處是可以節省磁盤文件和內存空間。每個節至少要占用一個內存頁。如果你能把可執行文件中節的數量從4個減少到3個,你就可以少占用一個內存頁。當然,這取決於這兩個被合並的節的未使用空間是否達到一頁。
對於合並節沒有什么硬性的規定。例如,可以合並.rdata到.text中,但你不應該把.rsrc,.reloc,或者.pdata合並到其它節中。在Visual Studio .NET之前,你可以合並.idata到其它節中。Visual Studio .NET,,就不允放過樣做了,但當鏈接一個發布版的時候,鏈接器經常合並.idata中的一部分到其它節中,例如.rdata。
既在一部分導入數據是當它們被加載到內存中時由加載器寫入的,你也許很奇怪它們怎么能被寫入一個只讀內存節。這是因為在加載時系統臨時把包含導入數據的頁面的屬性設為可讀寫。一旦導入表被初始化后,這些頁被設置回它們最初的保護屬性。
相對虛擬地址
在一個可執行文件中,有許多在內存中的地址必須被指定的位置。例如,當引用一個全局變量時就必須指定它的地址。PE文件可以被加載到進程地址空間的任何位置。雖然它們有一個首選加載地址,但你不能依賴於可執行文件真的會被加載到那個位置。因為這個原因,指定一個地址而不依賴於可執行文件的加載位置就很重要。
為了消除PE文件中對內存地址的硬編碼,於是產生了RVA。一個RVA是在內存中相對於PE文件被加載的地址的一個偏移。例如,如果一個EXE文件被加載到地址0x400000,它的代碼節位於地址0x401000處。那么代碼節的RVA就是:
(目標地址) 0x401000 - (加載地址)0x400000 = (RVA)0x1000.
要把一個RVA轉換為實際地址,進行相反的步驟就行了:把RVA和實際加載地址相加就可得到實際內存地址。順便說一下,實際內存地址在PE中被稱為虛擬地址(VA)。另外也可以認為一個VA是加上首選加載地址的RVA。不要忘了我以前說過的,加載地址和HMODULE是一樣的。
你是否想研究一下一些DLL在內存中的數據結構呢?這里有一個方法。以這個DLL的名字作為參數調用GetModuleHandle函數。返回的HMODULE是一個加載地址;你可以應用你的PE文件結構的知識找到這個模塊中的任何你想要的東西。
數據目錄
在可執行文件中有許多數據結構需要被快速定位。一些明顯的例子是導入表,導出表,資源,和基址重定位表。所有這些眾所周知的數據結構都可通過一致的方式被找到,就是數據目錄。
數據目錄是一個由16個結構組成的數組。每個數組元素都預定義了它所代表的含意。IMAGE_DIRECTORY_ENTRY_ xxx 定義了數據目錄的數組索引(從0到15)。圖2描述了每個IMAGE_DATA_DIRECTORY_xxx值分別表示了什么。這篇文章的第2部分包含了對其所指向的數據結構的更詳細的描述。
圖 2 IMAGE_DATA_DIRECTORY 值
值 |
描述 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
指向導出表(一個IMAGE_EXPORT_DIRECTORY結構)。 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
指向導入表(一個IMAGE_IMPORT_DESCRIPTOR結構數組)。 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
指向資源(一個IMAGE_RESOURCE_DIRECTORY結構。 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
指向異常處理表(一個IMAGE_RUNTIME_FUNCTION_ENTRY結構數組)。CPU特定的並且基於表的異常處理。用於除x86之外的其它CPU上。 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
指向一個WIN_CERTIFICATE結構的列表,它定義在WinTrust.H中。不會被映射到內存中。因此,VirtualAddress域是一個文件偏移,而不是一個RVA。 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
指向基址重定位信息。 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
指向一個IMAGE_DEBUG_DIRECTORY結構數組,其中每個結構描述了映像的一些調試信息。早期的Borland鏈接器設置這個IMAGE_DATA_DIRECTORY結構的Size域為結構的數目,而不是字節大小。要得到IMAGE_DEBUG_DIRECTORY結構的數目,用IMAGE_DEBUG_DIRECTORY 的大小除以這個Size域。 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
指向特定架構數據,它是一個IMAGE_ARCHITECTURE_HEADER結構數組。不用於x86或IA-64,但看來已用於DEC/Compaq Alpha。 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
在某些架構體系上VirtualAddress域是一個RVA,被用來作為全局指針(gp)。不用於x86,而用於IA-64。Size域沒有被使用。參見2000年11月的Under The Hood 專欄可得到關於IA-64 gp的更多信息。 |
IMAGE_DIRECTORY_ENTRY_TLS |
指向線程局部存儲初始化節。 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
指向一個IMAGE_LOAD_CONFIG_DIRECTORY結構。IMAGE_LOAD_CONFIG_DIRECTORY中的信息是特定於Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把這個結構放到你的可執行文件中,你必須用名字__load_config_used 定義一個全局結構,類型是IMAGE_LOAD_CONFIG_DIRECTORY。對於非x86的其它體系,符號名是_load_config_used (只有一個下划線)。如果你確實要包含一個IMAGE_LOAD_CONFIG_DIRECTORY,那么在 C++ 中要得到正確的名字比較棘手。鏈接器看到的符號名必須是__load_config_used (兩個下划線)。C++ 編譯器會在全局符號前加一個下划線。另外,它還用類型信息修飾全局符號名。因此,要使一切正常,在 C++ 中就必須像下面這樣使用: extern "C" IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...} |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結構數組,對應於這個映像綁定的每個DLL。數組元素中的時間戳允許加載器快速判斷綁定是否是新的。如果不是,加載器忽略綁定信息並且按正常方式解決導入API。 |
IMAGE_DIRECTORY_ENTRY_IAT |
指向第一個導入地址表(IAT)的開始位置。對應於每個被導入DLL的IAT都連續地排列在內存中。Size域指出了所有IAT的總的大小。在寫入導入函數的地址時加載器使用這個地址和Size域指定的大小臨時地標記IAT為可讀寫。 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
指向延遲加載信息,它是一個CImgDelayDescr結構數組,定義在Visual C++的頭文件DELAYIMP.H中。延遲加載的DLL直到對它們中的API進行第一次調用發生時才會被裝入。Windows中並沒有關於延遲加載DLL的知識,認識到這一點很重要。延遲加載的特征完全是由鏈接器和運行時庫實現的。 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
在最近更新的系統頭文件中這個值已被改名為IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可執行文件中.NET信息的最高級別信息,包括元數據。這個信息是一個IMAGE_COR20_HEADER結構。 |
導入函數
當你使用其它DLL中的代碼或數據時,就要導入它。加載一個PE文件時,Windows 加載器的一個工作就是查找所有被導入的函數和數據並讓那此函數和數據的地址可被加載的文件使用。完成這個工作所用到的數據結構的細節放到這篇文章的第二部分進行討論,在這里學習一下這些概念。
當你直接調用到一個DLL的代碼或數據時,你就是正在隱式地鏈接到這個DLL。要使被導入的API的地址可被你的代碼使用你不需要做任何事情。加載器會完成所有需要做的工作。另外還有顯式鏈接。意思就是說顯式地加載目標DLL並查找API的地址。這幾乎總是通過LoadLibrary和GetProcAddress來實現的。
當你隱式地鏈接一個API時,類似LoadLibrary和GetProcAddress的代碼仍然被執行了,只不過是由加載器代替你自動執行的。加載器也會確保被加載的PE文件所需要的任何附加的DLL也被加載。例如,由Visual C++®鏈接器創建的每個正常的程序都要鏈接KERNEL32.DLL。而KERNEL32.DLL又從NTDLL.DLL導入函數。同樣,如果你從GDI32.DLL導入函數,也將會依賴於USER32,ADVAPI32,NTDLL和KERNEL32 DLL。加載器會保證這些DLL都被加載並且解決所有導入問題。(Visual Basic 6.0和Microsoft .NET 可執行文件直接鏈接到另外一個DLL而不是KERNEL32,但原理是相同的。)
隱式鏈接時,對主EXE文件和所有依賴的DLL的處理發生在程序第一次啟動時。如果出現了任何問題(例如,一個被引用的DLL沒有找到),進程將被終止。
Visual C++ 6.0引入了延遲加載的功能,它是隱式鏈接和顯式鏈接的混合體。在延遲加載一個DLL時,鏈接器生成一些和正常導入一個DLL時非常相似的數據。然而,操作系統忽略這些數據。代替的,第一次調用一個延遲加載的API時,DLL才會被加載(如果還沒有加載到內存中),然后調用GetProcAddress方法得到被調用API的地址。以后如果再調用這個API將會和這個API被正常導入時有着一樣的效率。
在PE文件中,對於每個被導入的DLL都有一個數據結構的數組。這些結構給出被導入DLL的名稱並指向一個函數指針數組。這個函數指針數組就是導入地址表(IAT)。每個被導入的API在IAT中都有它自己的位置,導入函數的地址由Windows加載器寫入到那個位置中。最后一點非常重要:一旦一個模塊被加載,IAT中包含所要調用導入函數的地址。
IAT的優點是在一個PE文件中只有一個地方保存了被導入API的地址。不管源文件中多少次調用一個API,都會通過IAT中同一個函數指針來完成。
讓我們看一下怎樣調用一個被導入的API。需要考慮兩種情況:高效的和低效的。最好的情況,調用一個導入API看起來應該像下面這樣:
CALL DWORD PTR [0x00405030]
這是通過函數指針進行調用。無論怎樣,0x405030地址處的DWORD值就是這個CALL指令將把控制轉移到的地址。在前面例子中,地址0x405030就位於IAT中。
低效的調用看起來像下面這樣:
CALL 0x0040100C
...
0x0040100C:
JMP DWORD PTR [0x00405030]
這種情況下,CALL把控制轉到一個小的程序段處。這段程序通過JMP指令跳轉到0x405030地址處。記住0x405030位於IAT中。低效調用導入函數用到了五個字節的額外代碼,並且由於使用JMP指令花費了更長的執行時間。
你也許會奇怪為什么要使用低效的方法呢。有一個很好的解釋。編譯器無法區分導入函數調用和普通函數調用。因此,編譯器生成同樣形式的CALL指令
CALL XXXXXXXX
XXXXXXXX是一個稍后由鏈接器填充的實際地址。要注意這個CALL指令后面的地址並不是一個函數指針,而是一段實際代碼的地址。鏈接器必須提供一塊代碼來替換這個XXXXXXXX。這樣做的最簡單的方法就是調用到一個JMP stub,就像你在上面看到的那樣。
這個JMP stub從哪兒來呢?很令人驚奇,它來自於導入函數的導入庫。如果你檢查一個導入庫,並且用導入API的名稱來檢查代碼,你將會發現和上面JMP stub很相似的代碼。這就是說缺省情況下將使用低效形式調用導入API。
那么,下一個要問的問題就是怎樣才能得到優化的形式。答案是給編譯器一個提示。__declspec(dllimport)函數修飾符告訴編譯器這個函數位於其它DLL中,於是編譯器將生成指令
CALL DWORD PTR [XXXXXXXX]
而不是:
CALL XXXXXXXX
另外,編譯器也生成一些信息以告訴鏈接器把這個指令的函數指針部分解析為一個符號名__imp_functionname。例如,如果你正在調用MyFunction,符號名就是__imp_MyFunction。查看一個導入庫,你會發現除了正常的符號名外,也有一個加了__imp__前綴的符號。__imp__ symbol可以直接定位到IAT入口,而不是通過那個JMP stub。
那么這對你以后每天的生活有什么影響呢?如果你正在編寫導出函數並為它們提供一個頭文件,記住要使用這個__declspec(dllimport)修飾符:
__declspec(dllimport) void Foo(void);
如果你查看Windows系統頭文件,你會發現Windows API都使用了__declspec(dllimport)。它並不容易被發現。你可在WINNT.H頭文件中找到DECLSPEC_IMPORT 宏定義,而這個宏被用在一些文件中例如WinBase.H。到這里你就會明白__declspec(dllimport)是如何被用在系統API聲明上的。
我們知道,很多PE分析工具都可以查看一個EXE文件的引用DLL文件函數表,其實,這個本身就是存儲在EXE頭部的一個重要信息。
我們借用一張PE結構圖來分析:
一個EXE完整的PE結構分五大部分。見上圖.
MS-DOS頭
最開頭的是部分是DOS部首,DOS部首由兩部分組成:DOS的MZ文件標志和DOS stub(DOS存根程序)。之所以設置DOS部首是微軟為了兼容原有的DOS系統下的程序而設立的。
每個PE文件都以一個小的MS-DOS可執行體開頭。在Windows早期很多消費者並沒有安裝Windows,所以就需要存在這個MS-DOS可執行體。當在沒有安裝Windows的機器上執行時,這段程序至少能打印一條信息來說明必須在Windows上才能執行這個可執行文件。
PE文件以一個傳統的MS-DOS頭開頭,被稱為IMAGE_DOS_HEADER。其中只有兩個重要的值,它們是e_magic和e_lfanew。e_lfanew域包含PE頭的文件偏移。e_magic域(一個WORD)必須被設為0x5A4D。對於這個值有個常量定義,叫做IMAGE_DOS_SIGNATURE。用ASCII字符表示, 0x5A4D就是“MZ”,這是MS-DOS最初設計者之一Mark Zbikowski名子的首字母大寫。
DOS MZ header部分是DOS時代遺留的產物,是PE文件的一個遺傳基因,一個Win32程序如果在DOS下也是可以執行,只是提示:“This program cannot be run in DOS mode.”然后就結束執行,提示執行者,這個程序要在Win32系統下執行。
DOS stub 部分是DOS插樁代碼,是DOS下的16位程序代碼,只是為了顯示上面的提示數據。這段代碼是編譯器在程序編譯過程中自動添加的。
a.DOS頭的數據結構
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;
這個是winnt.h中定義的DOS頭數據結構,對於運行在win32上的程序,我們需要關心的是e_lfanew成員, 因為它指向了PE結構相對於文件頭的位置偏移,還有一個可能會用於簡單判斷文件類型的就是e_magic;它是IMAGE_DOS_SIGNATURE固定值'ZM'(0x5A4D)
b.PE文件結構
現在來讓我們研究PE文件的實際格式。我將從文件的開頭開始,並描述在每個PE文件中都會出現的數據結構。然后,我將描述在一個PE節中的更特殊的數據結構(例如導入表和資源)。下面我將討論的所有數據結構都定義在WINNT.H中,除非另有說明。
通常,這些結構都有 32 位和 64 位之分---例如 IMAGE_NT_HEADERS32 和IMAGE_NT_HEADERS64。這些結構除了一些域被擴展為 64 位外幾乎是一樣的。如果你正在試着編寫可移植的代碼,WINNT.H 文件中有一些 #defines 定義可以用來選擇使用32位還是 64 位的結構並且給它們起了一個與大小無關的別名(對於前面的例子這個別名就是IMAGE_NT_HEADERS)。具體選擇哪一個結構依賴於你正在以哪種模式編譯(是否定義了_WIN64)。只有在 PE 文件的目標執行平台的大小屬性與正在編譯的平台的大小屬性不同時才需要使用特定的 32 位或 64 位版本的結構。
PE頭信息
IMAGE_NT_HEADERS 結構是存儲 PE 文件細節信息的主要位置。它的偏移由這個文件開頭的 IMAGE_DOS_HEADER 的 e_lfanew 域給出。實際上有兩個版本的IMAGE_NT_HEADER 結構,一個用於 32 位可執行文件,另一個用於 64 位版本。它們之間的區別很小,在討論中我將認為它們是相同的。區別這兩種格式的唯一正確的、由Microsoft 認可的方法是通過 IMAGE_OPTIONAL_HEADER 結構(馬上就會講到)的 Magic 域的值。 typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
在一個有效的PE文件中,Signature字段的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_NT_SIGNATURE定義了這個值。第二個域是一個IMAGE_FILE_HEADER類型的結構,它包含了關於這個文件的一些基本的信息,最重要的是其中一個域指出了其后的可選數據的大小。在PE文件中,這個可選數據是必須的,但仍然被稱為IMAGE_OPTIONAL_HEADER。
圖3顯示了IMAGE_FILE_HEADER 結構的域以及對這些域的注釋。這個結構在COFF格式的OBJ文件開頭也可以找到。
圖 4 列出了IMAGE_FILE_xxx通常的取值。
圖5顯示了IMAGE_OPTIONAL_HEADER 結構的成員。
IMAGE_OPTIONAL_HEADER結構末尾的數據目錄數組用來定位可執行文件中的重要數據的地址。每個數據目錄條目看起來就像下面這樣:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA of the data
DWORD Size; // Size of the data
};
文件頭信息
值得注意的是PE文件頭中的IMAGE_OPTIONAL_HEADER32是一個非常重要的結構,PE文件中的導入表、導出表、資源、重定位表等數據的位置和長度都保存在這個結構里。
IMAGE_FILE_HEADER這個結構的定義如下:
圖 3 IMAGE_FILE_HEADER
大小 |
域 |
描述 |
WORD |
Machine |
可執行文件的目標CPU。通常的值是: IMAGE_FILE_MACHINE_I386 0x014c // Intel 386 IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64 |
WORD |
NumberOfSections |
指出節表中有多少個節。節表緊跟在IMAGE_NT_HEADERS之后。 |
DWORD |
TimeDateStamp |
指出這個文件被創建的時間。這個值是用格林尼治時間(GMT)計算的自從1970年1月1日以來所經過的秒數。這個值比文件系統的日期/時間更准確地指出了文件被創建的時間。使用_ctime 函數(對時區敏感)可以很容易地把這個值轉換為人們可讀的字符串形式。另一個有用的函數是gmtime。 |
DWORD |
PointerToSymbolTable |
COFF符號表的文件偏移,描述於Microsoft規范的5.4節。COFF符號表在PE文件中很少見,因為出現了新的調試格式。Visual Studio .NET之前,可通過指定鏈接器選項/DEBUGTYPE:COFF來創建COFF符號表。COFF符號表幾乎總是會出現在OBJ文件中。如果沒有符號表則設此值為0。 |
DWORD |
NumberOfSymbols |
如果存在COFF符號表,此域表示其中的符號的數目。COFF符號是一個固定大小的結構,要找到COFF符號表的末尾就必須用到此域。緊跟COFF符號之后是一個用來保存較長符號名的字符串表。 |
WORD |
SizeOfOptionalHeader |
IMAGE_FILE_HEADER 之后的可選數據的大小。在PE文件中,這個數據稱為IMAGE_OPTIONAL_HEADER。這個大小在32位和64位的文件中是不同的。對於32位PE文件,這個域通常是224。對於64位PE32+文件,它通常是240。然而,這些值只是所要求的最小值,更大的值也可能會出現。 |
WORD |
Characteristics |
一組指示文件屬性的位標。這些標記的有效值是定義於WINNT.H文件中的IMAGE_FILE_xxx值。一些常用的值在圖4中列出。 |
01.typedef struct _IMAGE_FILE_HEADER {
02.00h WORD Machine;//運行平台
03.02h WORD NumberOfSections;//區塊數目 pe文件中區塊的數量.
04.06h DWORD TimeDateStamp;//文件日期時間戳,指這個pe文件生成的時間,它的值是從1969年12月31日16:00:00以來的秒數. 05.0Ah DWORD PointerToSymbolTable;//指向符號表 Coff調試符號表的偏移地址.
06.0Eh DWORD NumberOfSymbols;//符號表中的符號數量 Coff符號表中符號的個數. 這個域和前個域在release版本的程序里是0.
07.12h WORD SizeOfOptionalHeader;//映像可選頭結構的大小 IMAGE_OPTIONAL_HEADER32結構的大小(即多少字節).我們接着就要提到這個結構了.事實上,pe文件的大部分重要的域都在IMAGE_OPTIONAL_HEADER結構里.
08.14hWORD Characteristics;//文件特征值
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
這個結構體表明一個PE文件的基本特征屬性,也是一個PE文件的入口
Machine域說明這個pe文件在什么CPU上運行,具體如下:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#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_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
Characteristics 這個域描述pe文件的一些屬性信息,比如是否可執行,是否是一個動態連接庫等.具體定義如下:
圖 4 IMAGE_FILE_XXX
值 |
描述 |
IMAGE_FILE_RELOCS_STRIPPED |
文件中不包括重定位信息。 |
IMAGE_FILE_EXECUTABLE_IMAGE |
文件是可執行的。 |
IMAGE_FILE_AGGRESIVE_WS_TRIM |
讓操作系統強制整理工作區。 |
IMAGE_FILE_LARGE_ADDRESS_AWARE |
應用程序可處理超過2GB的地址。 |
IMAGE_FILE_32BIT_MACHINE |
需要一個32位的機器。 |
IMAGE_FILE_DEBUG_STRIPPED |
調試信息位於一個.DBG文件中。 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP |
如果映像在可移動媒體中,那么復制到交換文件並從交換文件中運行。 |
IMAGE_FILE_NET_RUN_FROM_SWAP |
如果映像在網絡上,那么復制到交換文件並從交換文件中運行。 |
IMAGE_FILE_DLL |
是一個DLL文件。 |
IMAGE_FILE_UP_SYSTEM_ONLY |
只能在單處理器機器中運行。 |
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 重定位信息被移除 #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可執行 #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行號被移除 #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符號被移除 #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序能處理大於2G的地址 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed. #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位機器 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .dbg文件的調試信息被移除 #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果在移動介質中,拷到交換文件中運行 #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果在網絡中,拷到交換文件中運行 #define IMAGE_FILE_SYSTEM 0x1000 // 系統文件 #define IMAGE_FILE_DLL 0x2000 // 文件是一個dll #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 文件只能運行在單處理器上 #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
所以,根據這個結構體的信息,我們就可以判斷一個文件究竟是不是一個真正的PE文件,該PE文件的類型是可執行的還是可調用的(DLL)
我們可以寫個簡單的小程序來讀取這個信息:
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "conio.h"
int main(int argc,char* argv[])
{
FILE *p;
LONG e_lfanew;//指向IMAGE_NT_HEADERS32結構在文件中的偏移
IMAGE_FILE_HEADER myfileheader;
p =fopen("test1.exe","r+b");//自定義讀取的exe文件
if(p == NULL)return -1;//如果打開失敗就返回
fseek(p,0x3c,SEEK_SET);//注意這里是指針偏移,也就是繞過開頭的DOS區塊
fread(&e_lfanew,4,1,p);
fseek(p,e_lfanew+4,SEEK_SET);//指向IMAGE_FILE_HEADER結構的偏移
fread(&myfileheader,sizeof(myfileheader),1,p);
printf("IMAGE_FILE_HEADER結構:\n");
printf("Machine : %04X\n",myfileheader.Machine);
printf("NumberOfSections : %04X\n",myfileheader.NumberOfSections);
printf("TimeDateStamp : %08X\n",myfileheader.TimeDateStamp);
printf("PointerToSymbolTable : %08X\n",myfileheader.PointerToSymbolTable);
printf("NumberOfSymbols : %08X\n",myfileheader.NumberOfSymbols);
printf("SizeOfOptionalHeader : %04X\n",myfileheader.SizeOfOptionalHeader);
printf("Characteristics : %04X\n",myfileheader.Characteristics);
getch();
return 0;
}
注釋比較詳細了,大家根據這個就可以讀取一個PE文件的基本特征信息了.以上代碼VC6編譯通過
緊接着上一節,我們來研究下IMAGE_OPTIONAL_HEADER32,這個屬於PE中附加結構信息,同樣是很重要的。
我們先來看看它的結構:
圖 5 IMAGE_OPTIONAL_HEADER
Size |
Structure Member |
Description |
WORD |
Magic |
一個簽名,確定這是什么類型的頭。兩個最常用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b. |
BYTE |
MajorLinkerVersion |
創建可執行文件的鏈接器的主版本號。對於Microsoft的鏈接器生成的PE文件,這個版本號的Visual Studio的版本號相一致(例如,版本6表示Visual Studio 6.0)。 |
BYTE |
MinorLinkerVersion |
創建可執行文件的鏈接器的次版本號。 |
DWORD |
SizeOfCode |
所有具有IMAGE_SCN_CNT_CODE屬性的節的總的大小。 |
DWORD |
SizeOfInitializedData |
所有包含已初始數據的節的總的大小。 |
DWORD |
SizeOfUninitializedData |
所有包含未初始化數據的節的總的大小。這個域總是0,因為鏈接器可以把未初始化數據附加到常規數據節的末尾。 |
DWORD |
AddressOfEntryPoint |
文件中將被執行的第一個代碼字節的RVA。對於DLL,這個進入點將在進程初始化和關閉時以及線程被創建和銷毀時調用。在大多數可執行文件中,這個地址並不直接指向main,WinMain或DllMain函數,而是指向運行時庫代碼,由運行時庫調用前述函數。在DLL中,這個域可以被設為0,這樣的話上面所說的通知就不能被接收到。鏈接器選項/NOENTRY可以設置這個域為0。 |
DWORD |
BaseOfCode |
加載到內存后代碼的第一個字節的RVA。 |
DWORD |
BaseOfData |
理論上,它表示加載到內存后數據的第一個字節的RVA。然而,這個域的值對於不同版本的Microsoft鏈接器是不一致的。在64位的可執行文件中這個域不出現。 |
DWORD |
ImageBase |
文件在內存中的首選加載地址。加載器盡可能地把PE文件加載到這個地址(就是說,如果當前這塊內存沒有被占用,它是對齊的並且是一個合法的地址,等等)。如果可執行文件被加載到這個地址,加載器就可以跳過進行基址重定位(在這篇文章的第二部分描述)這一步。對於EXE,缺省的ImageBase是0x400000。對於DLL,缺省是0x10000000。在鏈接時可以通過/BASE 選項來指定ImageBase,或者以后用REBASE工具重新設置。 |
DWORD |
SectionAlignment |
加載到內存后節的對齊大小。這個值必須大於等於FileAlignment(下一個域)。缺省的對齊值是目標CPU的頁大上。對於運行在Windows 9x或Windows Me下的用戶模式可執行文件,最小對齊大小是一頁(4KB)。這個域可以通過鏈接器選項/ALIGN來設置。 |
DWORD |
FileAlignment |
在PE文件中節的對齊大小。對於x86下的可執行文件,這個值通常是0x200或0x1000。不同版本的Microsoft鏈接器缺省值不同。這個值必須是2的冪,並且如果SectionAlignment小於CPU的頁大小,這個域必須和SectionAlignment相匹配。鏈接器選項/OPT:WIN98可設置x86可執行文件的文件對齊為0x1000,/OPT:NOWIN98設置文件對齊為0x200。 |
WORD |
MajorOperatingSystemVersion |
所要求的操作系統的主版本號。隨着那么多版本Windows的出現,這個域的值就變得很不確切。 |
WORD |
MinorOperatingSystemVersion |
所要求的操作系統的次版本號。 |
WORD |
MajorImageVersion |
這個文件的主版本號。不被系統使用並可設為0。可以通過鏈接器選項/VERSION來設置。 |
WORD |
MinorImageVersion |
這個文件的次版本號。 |
WORD |
MajorSubsystemVersion |
可執行文件所要求的操作子系統的主版本號。它曾經被用來表示需要較新的Windows 95或Windows NT用戶界面,而不是老版本的Windows NT界面。今天隨着各種不同版本Windows的出現,這個域已不被系統使用,並且通常被設為4。可通過鏈接器選項/SUBSYSTEM設置這個域的值。 |
WORD |
MinorSubsystemVersion |
可執行文件所要求的操作子系統的次版本號。 |
DWORD |
Win32VersionValue |
另一個不被使用的域,通常設為0。 |
DWORD |
SizeOfImage |
映像的大小。它表示了加載文件到內存中時系統必須保留的內存的數量。這個域的值必須是SectionAlignmnet的倍數。 |
DWORD |
SizeOfHeaders |
MS-DOS頭,PE頭和節表的總的大小。PE文件中所有這些項目出現在任何代碼或數據節之前。這個域的值被調整為文件對齊大小的整數倍。 |
DWORD |
CheckSum |
映像的校驗和。IMAGEHLP.DLL中的CheckSumMappedFile函數可以計算出這個值。校驗和用於內核模式的驅動和一些系統DLL。對於其它的,這個域可以為0。當使用鏈接器選項/RELEASE時校驗和被放入文件中。 |
WORD |
Subsystem |
指示可執行文件期望的子系統(用戶界面類型)的枚舉值。這個域只用於EXE。一些重要的值包括: IMAGE_SUBSYSTEM_NATIVE// 映像不需要子系統IMAGE_SUBSYSTEM_WINDOWS_GUI// 使用Windows GUIIMAGE_SUBSYSTEM_WINDOWS_CUI// 作為控制台程序運行。// 運行時,操作系統創建一個控制台// 窗口並提供stdin,stdout和stderr// 文件句柄。 |
WORD |
DllCharacteristics |
標記DLL的特性。對應於IMAGE_DLLCHARACTERISTICS_xxx定義。當前的值是: IMAGE_DLLCHARACTERISTICS_NO_BIND// 不要綁定這個映像IMAGE_DLLCHARACTERISTICS_WDM_DRIVER// WDM模式的驅動程序IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE// 當終端服務加載一個不是// Terminal- Services-aware 的應用程// 序時,它也加載一個包含兼容代碼// 的DLL。 |
DWORD |
SizeOfStackReserve |
在EXE文件中,為線程保留的堆棧大小。缺省是1MB,但並不是所有的內存一開始都被提交。 |
DWORD |
SizeOfStackCommit |
在EXE文件中,為堆棧初始提交的內存數量。缺省情況下,這個域是4KB。 |
DWORD |
SizeOfHeapReserve |
在EXE文件中,為默認進程堆初始保留的內存大小。缺省是1MB。然而在當前版本的Windows中,堆不經過用戶干涉就能超出這里指定的大小。 |
DWORD |
SizeOfHeapCommit |
在EXE文件中,提交到堆的內存大小。缺省情況下,這里的值是4KB。 |
DWORD |
LoaderFlags |
不使用。 |
DWORD |
NumberOfRvaAndSizes |
在IMAGE_NT_HEADERS結構的末尾是一個IMAGE_DATA_DIRECTORY結構數組。此域包含了這個數組的元素個數。自從最早的Windows NT發布以來這個域的值一直是16。 |
IMAGE_ |
DataDirectory[16] |
一個IMAGE_DATA_DIRECTORY結構數組。每個結構都包含了可執行文件中一些重要數據的RVA和大小(例如導入表,導出表和資源)。 |
01.typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields. 標准域
00hWORD Magic;//幻數,32位pe文件總為010bh 32位pe文件總為010bh 這個常數的定義如下:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
02h BYTE MajorLinkerVersion;//連接程序的主版本號 如vc6.0的為06h 08.03hBYTE MinorLinkerVersion;//連接器副版本號 如vc6.0的為00h
04h DWORD SizeOfCode;//代碼段總大小 pe文件代碼段的大小.是FileAlignment的整數倍.
08h DWORD SizeOfInitializedData;//所有含已初始化數據的塊的大小,一般在.data段中.
0ch DWORD SizeOfUninitializedData;//所有含未初始化數據的塊的大小,一般在.bss段中
10h DWORD AddressOfEntryPoint;//程序執行入口地址(RVA) 程序開始執行的地址,這是一個RVA(相對虛擬地址).對於exe文件,這里是啟動代碼;
//對於dll文件,這里是libMain()的地址. 在脫殼時第一件事就是找入口點,指的就是這個值.
14h DWORD BaseOfCode;//代碼段起始地址(RVA) 代碼段基地址,微軟的連接程序生成的程序一般把這個值置為1000h
18h DWORD BaseOfData;//數據段起始地址(RVA) 數據段基地址
// NT additional fields.
1ch DWORD ImageBase;//pe文件默認的裝入起始地址.windows9x中exe文件為400000h,dll文件為10000000h
20h DWORD SectionAlignment;//內存中區塊的對齊單位.區塊總是對齊到這個值的整數倍.x86的32位系統上默認值位1000h
24h DWORD FileAlignment;//文件中區塊的對齊單位; pe文件中默認值為 200h.
28h WORD MajorOperatingSystemVersion;//所需操作系統主版本號
2ah WORD MinorOperatingSystemVersion;//所需操作系統副版本號 上面兩個域是指運行這個pe文件所需的操作系統的最低版本號.windows95/98和windows nt 4.0 的內部版本號都是 4.0 ,而windows2000的內部版本號是5.0
2ch WORD MajorImageVersion;//自定義主版本號
2eh WORD MinorImageVersion;//自定義副版本號 上面兩個域是指用戶自定義的pe文件的版本號.可以通過連接程序來設置,如: LINK /VERSION:2.0 MyApp.obj一般在升級時使用.
30h WORD MajorSubsystemVersion;//所需子系統主版本號
32h WORD MinorSubsystemVersion;//所需子系統副版本號 上面兩個域是指運行這個pe文件所要求的子系統的版本號.
34h DWORD Win32VersionValue;//總是0
38h DWORD SizeOfImage;//pe文件裝入內存后映像的總大小.如果SectionAlignment域和FileAlignment域相等,那么這個值也是pe文件在硬盤上的大小.
3ch DWORD SizeOfHeaders;//從pe文件開始到節表(包含節表)的總大小 .其后是各個區段的數據.
40h DWORD CheckSum;//pe文件CRC校驗和
44h WORD Subsystem;//用戶界面使用的子系統類型,見后面
46h WORD DllCharacteristics;//為0
48h DWORD SizeOfStackReserve;//為線程的棧初始保留的虛擬內存的默認大小,默認為00100000h.如果在調用CreateThread函數時指定
//堆棧的大小為0,被創建的線程的堆棧的初始大小就與這個值相同.
4ch DWORD SizeOfStackCommit;// 為線程的棧初始提交的虛擬內存的大小.微軟的連接程序把這個值置為 1000h.
50h DWORD SizeOfHeapReserve;// 為進程的堆保留的虛擬內存的大小.默認值為 00100000h.
54h DWORD SizeOfHeapCommit;//為進程的堆初始提交的虛擬內存的大小 微軟的連接程序把這個值置為1000h.
58h DWORD LoaderFlags;//通常為0
5ch DWORD NumberOfRvaAndSizes;//數據目錄結構數組的項數,總為 00000010h 這個值定義如下: #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
60h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//數據目錄結構數組
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
這個結構體非常的龐大,大家通過注釋可以看出,這個結構體保存了相當全面的PE附件信息。
Subsystem pe文件的用戶界面使用的子系統類型.定義如下:
#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.
IMAGE_DATA_DIRECTORY DataDirectory[0x10]
數據目錄結構數組
IMAGE_DATA_DIRECTORY結構定義如下:
1.typedef struct _IMAGE_DATA_DIRECTORY {
2.DWORD VirtualAddress;// 相對虛擬地址
3.DWORD Size;//大小
4.} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
這個結構包含了pe文件中重要部分的RVA地址和大小.這個數組使操作系統的加載程序能夠快速定位特定的區段.具體定義如下:
#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
節表
IMAGE_NT_HEADERS之后緊跟着節表。節表是一個IMAGE_SECTION_HEADER結構數組。IMAGE_SECTION_HEADER提供了和它關聯的節的信息,包括位置,長度和屬性。圖6描述了IMAGE_SECTION_HEADER結構的各域。在IMAGE_FILE_HEADER結構中的NumberOfSections 域中提供了IMAGE_SECTION_HEADER 結構的數目。
可執行文件中的節的文件對齊對最終的文件大小有很大的影響。在Visual Studio 6.0中, 鏈接器缺省的對齊大小為4KB,除非使用了/OPT:NOWIN98或/ALIGN選項。Visual Studio .NET鏈接器也缺省使用了/OPT:WIN98選項,但它檢測可執行文件的大小是否小於某個值,如果是則使用0x200字節進行對齊。
另一個值得注意的對齊方式來自.NET文件規范。它規定.NET可執行文件的內存對齊值是8KB,而不是x86平台的4KB。這是為了保證在x86平台上創建的可執行文件在IA-64平台上仍然可以運行。如果節的內存對齊值是4KB,IA-64加載器就不能加載這個文件,因為64位Windows的頁大小是8KB。
圖 6 IMAGE_SECTION_HEADER
大小 |
域 |
描述 |
BYTE |
Name[8] |
節的ASCII名稱。節名不保證一定是以NULL結尾的。如果你指定了長於8個字符的節名,鏈接器會把它截短為8個字符。在OBJ文件中存在一個機制允許更長的節名。節名通常以一個句點開始,但這並不是必須的。節名中有一個“$”時鏈接器會對之進行特殊處理。前面帶有“$”的相同名字的節將會被合並。合並的順序是按照“$”后面字符的字母順序進行合並的。關於名字中帶有“$”的節以及這些節怎樣被合並有很多的主題,但這些細節已超出本文所討論的范圍了。 |
DWORD |
Misc.VirtualSize |
指出實際被使用的節的大小。這個域的值可以大於或小於SizeOfRawData域的值。如果VirtualSize的值大,SizeOfRawData就是可執行文件中已初始化數據的大小,剩下的字節用0填充。在OBJ文件中這個域被設為0。 |
DWORD |
VirtualAddress |
在可執行文件中,是節被加載到內存中后的RVA。在OBJ文件中應該被設為0。 |
DWORD |
SizeOfRawData |
在可執行文件或OBJ文件中該節所占用的字節大小。對於可執行文件,這個值必須是PE頭中給出的文件對齊值的倍數。如果是0,則說明這個節中的數據是未初始的。 |
DWORD |
PointerToRawData |
節在磁盤文件中的偏移。對於可執行文件,這個值必須是PE頭部給出的文件對齊值的倍數。 |
DWORD |
PointerToRelocations |
節的重定位數據的文件偏移。只用於OBJ文件,在可執行文件中被設為0。對於OBJ文件,如果這個域的值不為0的話,它就指向一個IMAGE_RELOCATION結構數組。 |
DWORD |
PointerToLinenumbers |
節的COFF樣式行號的文件偏移。如果非0,則指向一個IMAGE_LINENUMBER結構數組。只在COFF行號被生成時使用。 |
WORD |
NumberOfRelocations |
PointerToRelocations 指向的重定位的數目。在可執行文件中應該是0。 |
WORD |
NumberOfLinenumbers |
NumberOfRelocations 域指向的行號的數目。只在COFF行號被生成時使用。 |
DWORD |
Characteristics |
被或到一起的一些標記,用來表示節的屬性。這些標記中很多都可以通過鏈接器選項/SECTION來設置。常用值在圖7中列出。 |
c.Sections的目錄
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //標識字,表示是可執行鏡像還是ROM鏡像 union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //目標文件是重定位的地址,執行文件是鏡像的大小
DWORD VirtualAddress; 加載到內存后的相對地址
DWORD SizeOfRawData; Section原始數據的大小,FileAlignment 對齊
DWORD PointerToRawData; 原始數據的文件偏移
DWORD PointerToRelocations; 重定位信息的數據位置
DWORD PointerToLinenumbers; 行數據的位置
WORD NumberOfRelocations; 重定向信息的數目
WORD NumberOfLinenumbers; 行數據的項數
DWORD Characteristics; Section的屬性配置 ,詳細見下面
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
圖 7 Flags
值 |
描述 |
IMAGE_SCN_CNT_CODE |
節中包含代碼。 |
IMAGE_SCN_MEM_EXECUTE |
節是可執行的。 |
IMAGE_SCN_CNT_INITIALIZED_DATA |
節中包含已初始化數據。 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA |
節中包含未初始化數據。 |
IMAGE_SCN_MEM_DISCARDABLE |
節可被丟棄。用於保存鏈接器使用的一些信息,包括.debug$節。 |
IMAGE_SCN_MEM_NOT_PAGED |
節不可被頁交換,因此它總是存在於物理內存中。經常用於內核模式的驅動程序。 |
IMAGE_SCN_MEM_SHARED |
包含節的數據的物理內存頁在所有用到這個可執行體的進程之間共享。因此,每個進程看到這個節中的數據值都是完全一樣的。這對一個進程的所有實例之間共享全局變量很有用。要使一個節共享,可使用/section:name,S 鏈接器選項。 |
IMAGE_SCN_MEM_READ |
節是可讀的。幾乎總是被設置。 |
IMAGE_SCN_MEM_WRITE |
節是可寫的。 |
IMAGE_SCN_LNK_INFO |
節中包含鏈接器使用的信息。只在OBJ文件中存在。 |
IMAGE_SCN_LNK_REMOVE |
節中的數據不會成為映像的一部分。只出現在OBJ文件中。 |
IMAGE_SCN_LNK_COMDAT |
節中的內容是公共數據(comdat)。公共數據是指可被定義在多個OBJ文件中的數據。鏈接器將選擇一個包含到可執行文件中。Comdat 對於支持C++模板函數和在函數級別上的鏈接是至關重要的。Comdat節只出現在OBJ文件中。 |
IMAGE_SCN_ALIGN_XBYTES |
在最終的可執行文件中這個節中數據的對齊大小。它可有許多取值(_4BYTES,_8BYTES,_16BYTES等)。如果沒有被指定,缺省是16字節。這些標記只在OBJ文件中被設置。 |
Characteristics 它描述了這個Section的元屬性;
IMAGE_SCN_CNT_CODE表示節的內容保護可執行代碼
IMAGE_SCN_CNT_INITIALIZED_DATA含有已經初始化的數據
IMAGE_SCN_CNT_UNINITIALIZED_DATA含有未初始化數據,需要在加載時初始化為全0
IMAGE_SCN_LNK_INFO連接器信息,是目標文件的一部分
IMAGE_SCN_LNK_REMOVE連接后是否可以丟棄,對目標文件有效
IMAGE_SCN_LNK_COMDAT數據是連接通用數據
IMAGE_SCN_MEM_FARDATA(內存遠程數據節)
IMAGE_SCN_MEM_PURGEABLE(內存可清除節),使用后可以把內存清除?還是加載后就可以清除?
IMAGE_SCN_MEM_LOCKED內存不能被移出?
IMAGE_SCN_MEM_PRELOAD內存需要預先加載?數據對齊方式,只用於目標文件
IMAGE_SCN_LNK_NRELOC_OVFL表示重定向的數目大於0xffff,真正的數據會保存在第一個relocationSection的VirtualAddress中? IMAGE_SCN_MEM_DISCARDABLE,如果有需要,該Section占有的內存可以被丟棄?意思是在加載成功后就可以丟棄嗎?
IMAGE_SCN_MEM_NOT_CACHED,這節的內存不能被cache?是不是指每次都要重新從磁盤里讀?這個東西會被修改?
IMAGE_SCN_MEM_NOT_PAGED Section不能被頁交換出內存
IMAGE_SCN_MEM_SHARED表示所有的實例都共享同一個內存鏡像,對於DLL有效,這個只對數據Section有意義的,因為代碼Section都是寫拷貝(這里拷貝不應該被理解為有代碼功能拷貝操作,只是里面的地址會被修正)的,因為在執行重定向的時候,會映射到不同的地址。 IMAGE_SCN_MEM_EXECUTE Section的內容可以被執行
IMAGE_SCN_MEM_READ,可讀
IMAGE_SCN_MEM_WRITE, 可寫
Section Headers是一個數組,但是綁定section header后會馬上跟着Section的內容。這個也是NumberOfSections存在的目的,因為這樣才可以精確訪問各個Section,而不能簡單通過枚舉,並當遇到全0的Section Header時停。
d.代碼Section
i.IMAGE_OPTIONAL_HEADER32的BaseOfCode將指向這個Section的開始處。AddressOfEntryPoint則指向這個Section中中的某個位置這個Section的標志至少需要設置IMAGE_SCN_CNT_CODE,IMAGE_SCN_MEM_EXECUTE,IMAGE_SCN_MEM_READ這3個標志典型的Section名稱: ".text", ".code", "AUTO"
e.數據Section
已初始化的數據段,包括已初始化的全局數據和已經初始化的靜態變量,這個Section的標志至少為; IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_WRITE, IMAGE_SCN_MEM_READ 已經初始化的數據Section可能有多個,但是它們都會在IMAGE_OPTIONAL_HEADER32所表示的 BaseOfData + SizeOfInitializedData范圍內 典型的Section名稱有:".data", ".idata", "DATA"
f.BSS Section
未初始化數據Section,包括沒有初始化的全局變量和靜態變量,這種Section的PointToRawData 是0,Section的標志變量包括IMAGE_SCN_CNT_UNINITIALIZED_DATA,IMAGE_SCN_MEM_WRITE, IMAGE_SCN_MEM_READ而整個數據長度由IMAGE_OPTIONAL_HEADER32的SizeOfUninitializedData表示,而且它的初始化需要由PE Loader 來完成。 典型的Section名稱有:".bss", "BSS" g.棧Section和堆Section並不保存在PE中,而是由PE Loader根據IMAGE_OPTIONAL_HEADER32設置的堆棧大小創建
h.版權Section
目錄結構的IMAGE_DIRECTORY_ENTRY_COPYRIGHT下標的內容是一個以ASCII的描述的字符串,通常它是通過參數的形式傳給連接器的,這個串並不以0結尾的。這個Section是不能寫的。
i.輸入地址表Section
對於編譯器而言發現對外部符號的調用時,它只會直接生成對那個符號的調用指令。但是連接器就需要為每個 輸入的函數符號設置調用stub,這個stub就是跳轉到目的地址,程序員可以通過"__declspec(dllimport)"來 避免生成stub,因為編譯器會自己去計算,連接器就不要生成stub了。 stub的集合就是"轉移區",通常它位於代碼Section中,連接器並不知道這些地址的真正的值,它需要PE loader 在加載的時候修正。 轉移區的結構:
_symbol: jmp [__imp__symbol]
_other_symbol: jmp [__imp__other__symbol]
而"__imp__symbol", "__imp__other__symbol"這些符號的真正地址值是需要修正的,而且可以通過IMAGE_DIRECTORY_ENTRY_IAT從IMAGE_DATA_DIRECTORY獲得。
輸入地址轉換表通常是由函數輸出者如:DLL等提供給連接器使用的,事實上地址轉換表並不是必須的,因為加載器可以通過加載者的輸入符號表和依賴文件(DLL)輸出符號表來修正。 地址轉換表從概念上是輸入導入輸入目錄的范疇,但它實際上是一個獨立的Section。
j.輸入符號表Section
輸入符號Section的內部數據(當然,不會包含一個Section Header)是一個IMAGE_IMPORT_DESCRIPTOR數組
這個Section屬性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ
通過IMAGE_DIRECTORY_ENTRY_IMPORT可以得到第一個IMAGE_IMPORT_DESCRIPTOR的位置
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
//原來的導入函數名數組數組首地址RVA ORG
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) 數組的成員是IMAGE_THUNK_DATA結構
};
DWORD TimeDateStamp; // 0 if not bound, 時間戳(作用比較復雜) 1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders 中轉鏈 這個數據一般為0,可以不關心
DWORD Name; RVA,指向DLL名字的指針,ASCII字符串
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) 函數轉換后的地址,
//指向一個 IMAGE_THUNK_DATA 結構數組的RVA,這個數據與IAT所指向的地址一致
} IMAGE_IMPORT_DESCRIPTOR;
IMAGE_THUNK_DATA 這是一個DWORD類型的集合。通常我們將其解釋為指向一個 IMAGE_IMPORT_BY_NAME 結構的指針,
其定義如下:
IMAGE_THUNK_DATA{
union
{ PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;//判定當前結構數據是不是以序號為輸出的,如果是的話該值為0x800000000,此時PIMAGE_IMPORT_BY_NAME不可做為名稱使用 PIMAGE_IMPORT_BY_NAME AddressOfData;
}u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;// 函數輸出序號 導入的DLL的輸出名字表的索引
BYTE Name1[1];//輸出函數名稱 BYTE 0結尾的ASCII字符串(函數名)
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME
這里有兩個指向導入函數信息的RVA數組ORG和TAR,使用方式是這樣的, PE Loader 查造執行文件的導入符號表,優先通過導入DLL的函數索引導出表中查找函數,如果失敗就使用名字查找,得到最終的轉換地址后就將這個線性地址保存在一個"地方",然后把這個"地方"的地址填到TAR 對應的IMAGE_THUNK_DATA中,這個地方就是地址轉換表,但是並不是所有的連接器都會生成可以通過IMAGE_DIRECTORY_ENTRY_IAT訪問。(如果沒有IAT,那么只要將線性地址直接放到IMAGE_THUNK_DATA中)
這里需要有兩個地方需要注意:
1.IMAGE_THUNK_DATA的值最高位為1時表示不包含導入函數的名字。也就是它不是指向IMAGE_IMPORT_BY_NAME的RVA,我們可以通過它的低地址的WORD得到序數
2."綁定"事實上就是限定導入的DLL的加載地址,然后就能在連接的時候設置TAR的值,PE Loader就能節省時間;當DLL的版本不對,或者重定向必須發生時,ORG仍然提供足夠的信息讓PE Loader來修正地址映射。
重定向的發生Loader是知道的,而DLL版本是通過時間戳來判斷的,如果時間戳為0,表示沒有綁定,如果非0,就需要和DLL里Header的時間戳對比,只有一致時,才不需要進行導入地址的修正。
3.對於中轉的情況,也就是引用的DLL中導出了一個不在本身定義的符號,這時修正是必須發生的。
4.中轉鏈的值是TAR的下標,它表示這個符號是中轉的,而這TAR的內容就是下一個中轉的下標,一直到(-1)表示沒有中轉了
k.新式綁定
它不是一個獨立的Section,而是放到Section Headers后面,第一節之前,通過IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT可以轉向到數據開頭IMAGE_BOUND_IMPORT_DESCRIPTOR 所有導入符號的地址都已經被事先修正,而不管它是不是中轉的。 typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR
{
DWORD TimeDateStamp;// 時間戳,為-1 是有效的,顯示版本
WORD OffsetModuleName; // DLL 名稱相對目錄開頭的偏移 模塊名稱偏移
WORD NumberOfModuleForwarderRefs; //DLL中轉的其他DLL的數目 未使用 Array of zero or more IMAGE_BOUND_FORWARDER_REF
//follows多個IMAGE_BOUND_FORWARDER_REF
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
從這種新的綁定方式來看,我不明白它能帶來什么樣的用處,如果只是強制所有的符號都進行重定位的話 那只需要強制PE loader完成地址修正就可以了。
它的作用感覺只是強調了綁定的信息,通過單獨列出DLL 的"版本"來決定是否需要重新計算
l.輸出符號表Section 輸出符號表通常存在於DLL中,通過IMAGE_DIRECTORY_ENTRY_EXPORT可以直接得到數據的入口,它的屬性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ, 不可丟棄,典型的段名稱:".edata"
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;DLL的特性,保留
DWORD TimeDateStamp; 時間戳,不一定有效
WORD MajorVersion; 主版本
WORD MinorVersion; 次版本號
DWORD Name;名字的RVA
DWORD Base;基址(就是導出函數的起始下標)
DWORD NumberOfFunctions; 輸出的函數數目
DWORD NumberOfNames; 輸出的名字的數目
DWORD AddressOfFunctions; // RVA from base of image 輸出的函數地址數組地址的RVA DWORD AddressOfNames; // RVA from base of image 輸出函數名字數組地址的RVA DWORD AddressOfNameOrdinals; // RVA from base of image 函數名字對應的輸出函數所在 AddressOfFunction的下標 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
這里需要說明的是AddressOfFunction、AddressOfNames、AddressOfNameOrdinals的使用方法:
1.如果通過函數的編號來查找函數,那么首先通過(編號-Base)得到AddressOfFunction的下標這樣就可以直接得到查找函數的RVA
2.如果通過函數名字查找函數,那么首先從AddressOfNamesz中查找對應的名字,如果找到,比如 在下標為10的為位置,那么就用10去索引AddrssOfNameOrdinals數組,從而得到查找函數在 AddressOfFunction中的位置,通過這個位置信息就能得到查找函數的RVA
m.資源Section
該Section至少包含IMAGE_SCN_CNT_INITIALIZED_DATA、IMAGE_SCN_MEM_READ標志。 可以通過IMAGE_RESOURCE_DIRECTORY_ENTRY索引IMAGE_DATA_DIRECTORY得到相應的RVA,資源結構 是通過IMAGE_RESOURCE_DIRECTORY來描述的 typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; 資源屬性,保留
DWORD TimeDateStamp; 時間戳,資源生成時間
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;資源名稱的數目
WORD NumberOfIdEntries;資源ID的數目 // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
緊跟着的是IMAGE_RESOURCE_DIRECTORY_ENTRY
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
它由兩個DWORD組成,它的含義分別如下:
1.第一個DWORD,如果他的最高位為1,那么表示剩下的31位表示一個相對於資源開始位置的偏移,偏移內容是IMAGE_RESOURCE_DIR_STRING,它標識這個IMAGE_RESOURCE_DIRECTORY_ENTRY;如果最 高位為0時,就表示這個DWORD的低16位(WORD)是表示IMAGE_RESOURCE_DIRECTORY_ENTRY的ID。
2.第二個DWORD,如果它的最高位是1,表示它還有下一層結構(也不是它本身不表表示資源內容), 剩下的31位是相對於資源開始位置的偏移,偏移的內容是下一個IMAGE_RESOURCE_DIRECTORY_ENTRY 如果最高位為0,表示沒有下一層結構了,剩下的31位也是偏移,偏移的內容是 IMAGE_RESOURCE_DATA_ENTRY,這個結構會說明資源的具體信息。
(資源的開始位置實際上就是IMAGE_DATA_DIRECTORY[IMAGE_RESOURCE_DIRECTORY_ENTRY]) 通常我們會使用ID來表示資源,但也通過IMAGE_RESOURCE_DIR_STRING來表示
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; 資源名稱的長度
WCHAR NameString[ 1 ]; 資源名稱
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; 實際數據的RVA
DWORD Size; 資源的大小,以字節為單位
DWORD CodePage; 通常是Unicode code page
DWORD Reserved; Reserved
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
在到達真正的資源描述結構IMAGE_RESOURCE_DATA_ENTRY之前,通常需要經過3層結構: 資源類型(bmp/menu)-->資源名-->資源的不同語言版本-->IMAGE_RESOURCE_DATA_ENTRY
n.重定位Section
基址重定位目錄通過IMAGE_DIRECTORY_ENTRY_BASERELOC索引IMAGE_DATA_DIRECTORY得到, 它的屬性標志至少包括IMAGE_SCN_CNT_INITIALIZED_DATA、 IMAGE_SCN_MEM_DISCARDABLE和IMAGE_SCN_MEM_READ Section的典型名稱是".reloc",如果鏡像不能加載到預定的位置,那么重定位信息就是必須的,鏈接器提供的地址就不再有效,PE Loader需要對靜態變量,字符串變量使用絕對的地址進行訪問。 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress;重定位目標塊基本的RVA DWORD SizeOfBlock;重定位塊數據的大小 // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; 重定位信息是一些連續的"塊",每一"塊"包含4K的重定位信息。 每一"塊"數據由IMAGE_BASE_RELOCATION + 實際的重定位數據,每一條數據都是16位的。IMAGE_BASE_RELOCATION后的16bit的數據由高4bit的標志位和低12bit的位置信息含義:(實際上我們只要關心兩種重定位類型,不需要任何操作和全替換操作)
1.當標志位為IMAGE_REL_BASED_ABSOLUTE (0),表示只用於字節對齊,不需要操作
2.當標志位為IMAGE_REL_BASED_HIGHLOW (3),表示由(12bit的值 + 塊基址)的RVA指向的DWORD內容需要被計算后的修正地址替換。
1、該結構后面緊跟的是word型的重定位項。
2、重定位地址的 RVA = VirtualAddress + word型重定位項的低12位
3、word行重定位項的高4位表示重定位類型,一般常見的值為0和3
高4位值 | 常量表示 | 含義 |
0 | IMAGE_REL_BASED_ABSOLUTE | 使塊按照32位對齊,位置為0 |
1 | IMAGE_REL_BASED_HIGH | 高16位必須應用於偏移量所指高字16位 |
2 | IMAGE_REL_BASED_LOW | 低16位必須應用於偏移量所指低字16位 |
3 | IMAGE_REL_BASED_HIGHLOW | 全部32位應用於所有32位 |
4 | IMAGE_REL_BASED_HIGHADJ | 需要32位,高16位位於偏移量,低16位位於下一個偏移量數組元素,組合為一個帶符號數,加上32位的一個數,然后加上8000然后把高16位保存在偏移量的16位域內 |
5 | IMAGE_REL_BASED_MIPS_JMPADDR | 資料不詳 |
6 | IMAGE_REL_BASED_SECTION | 資料不詳 |
7 | IMAGE_REL_BASED_REL32 | 資料不詳 |
4、重定位項數 = (SizeOfBlock - 4 - 4) / 2
5、重定位塊結束,以IMAGE_BASE_RELOCATION結構的VirtualAddress值為0結束。因此若映像加載00400000h,則代碼加載地址為00401000h
本文內容個人通過網絡,整理,收集。