先把PE文件介紹完。。
Section字段
text section 此節一般包含有連接器連接的所有obj目標文件的執行代碼。 這個執行代碼塊是一個大的.text。不同於在DOS下面的執行文件可以分成幾部分。 如果是使用的Borland C++,其編譯器將產生的代碼存於名為CODE的區域, 連接器連接到名為CODE而不是.text的節中。 data section .data是初始化的數據塊。這些數據塊包括編譯時被初始化的字符串常量、全局(globle)和靜態(static)變量。 bss section 任何沒有初始化的全局和局部變量都會存放到.bss節中。 這個節並不佔用文件的儲藏空間,所以 RawDataOffset 總是為0。 rsrc section 該節包含模塊的全部資源。如圖標、菜單、位圖等等。 idata section .idata包含其他外來的如DLL中的函數及數據信息。PE文件的每一個輸入函數都明確的列於該節中。 edata section 與.idata對應,.edata是該PE文件輸出函數和數據的列表,以供其他模塊引用。 其他部分 有的PE文件沒有引出函數或數據,也就沒有該節。 在節(Sections)的後面是COFF符號表格、COFF調試信息、COFF行號信息。
這個是我在PPT上找的圖,比較好理解。
導出表導入表(重點)(_IMAGE_EXPORT_DIRECTORY和_IMAGE_IMPORT_DESCRIPTOR)
導出表就是當前的PE文件提供了那些函數給別人用。
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; // DWORD TimeDateStamp; //時間戳. 編譯的時間. 把秒轉為時間.可以知道這個DLL是什麼時候編譯出來的. WORD MajorVersion; WORD MinorVersion; DWORD Name; //指向該導出表文件名的字符串,也就是這個DLL的名稱 輔助信息.修改不影響 存儲的RVA 如果想在文件中查看.自己計算一下FOA即可. DWORD Base; // 導出函數的起始序號 DWORD NumberOfFunctions; //所有的導出函數的個數 DWORD NumberOfNames; //以名字導出的函數的個數 DWORD AddressOfFunctions; // 導出的函數地址的 地址表 RVA 也就是 函數地址表 DWORD AddressOfNames; // 導出的函數名稱表的 RVA 也就是 函數名稱表 DWORD AddressOfNameOrdinals; // 導出函數序號表的RVA 也就是 函數序號表 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0表示終止零導入描述符 DWORD OriginalFirstThunk; // RVA到原始未綁定IAT(PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; // 沒綁定, // -1(如果綁定)和實際日期/時間戳 // 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 DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; 使用RtlImageDirectoryEntryToData並將索引號傳1,會得到一個如上結構的指針,實際上指向一個上述結構的數組,每個導入的DLL都會成為數組中的一項,也就是說,一個這樣的結構對應一個導入的DLL。 Characteristics和OriginalFirstThunk:一個聯合體,如果是數組的最後一項Characteristics為0,否則OriginalFirstThunk保存一個RVA,指向一個IMAGE_THUNK_DATA的數組,這個數組中的每一項表示一個導入函數。 TimeDateStamp:映象綁定前,這個值是0,綁定後是導入模塊的時間戳。 ForwarderChain:轉發鏈,如果沒有轉發器,這個值是-1。 Name:一個RVA,指向導入模塊的名字,所以一個IMAGE_IMPORT_DESCRIPTOR描述一個導入的DLL。 FirstThunk:也是一個RVA,也指向一個IMAGE_THUNK_DATA數組。 既然OriginalFirstThunk與FirstThunk都指向一個IMAGE_THUNK_DATA數組,而且這兩個域的名字都長得很像,他倆有什麼區別呢?為了解答這個問題,先來認識一下IMAGE_THUNK_DATA結構:
雖然我截圖的後面FOA已經計算出來,但是,我們最好自己會算,知道在哪裡找這些字段有什麼用。下面是dll的導入表。
重定位(struct BASE_RELOCATION_TABLE RelocTable)
Windows使用重定位機制保證代碼無論模塊加載到哪個基址都能正確被調用,其實原理並不很複雜,這個過程分三步:
1.編譯的時候由編譯器識別出哪些項使用了模塊內的直接VA,比如push一個全局變量、函數地址,這些指令的操作數在模塊加載的時候就需要被重定位。
2.鏈接器生成PE文件的時候將編譯器識別的重定位的項紀錄在一張表裡,這張表就是重定位表,保存在DataDirectory中,序號是
IMAGE_DIRECTORY_ENTRY_BASERELOC。
3.PE文件加載時,PE 加載器分析重定位表,將其中每一項按照現在的模塊基址進行重定位。
哪些項目需要被重定位呢?
1.代碼中使用全局變量的指令,因為全局變量一定是模塊內的地址,而且使用全局變量的語句在編譯後會產生一條引用全局變量基地址的指令。
2.將模塊函數指針賦值給變量或作為參數傳遞,因為賦值或傳遞參數是會產生mov和push指令,這些指令需要直接地址。
3.C++中的構造函數和析構函數賦值虛函數表指針,虛函數表中的每一項本身就是重定位項。
按照常規思路,每個重定位項應該是一個DWORD,裡面保存需要重定位的RVA,這樣只需要簡單操作便能找到需要重定位的項。然而,Windows並沒有這樣設計,原因是這樣存放太佔用空間了,試想一下,加入一個文件有n個重定位項,那麼就需要佔用4*n個字節。所以Windows採用了分組的方式,按照重定位項所在的頁面分組,每組保存一個頁面其實地址的RVA,頁內的每項重定位項使用一個WORD保存重定位項在頁內的偏移,這樣就大大縮小了重定位表的大小。
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; VirtualAddress:頁起始地址RVA。 SizeOfBlock:表示該分組保存了幾項重定位項。 TypeOffset:這個域有兩個含義,大家都知道,頁內偏移用12位就可以表示, 剩下的高4位用來表示重定位的類型。 而事實上,Windows只用了一種類型IMAGE_REL_BASED_HIGHLOW 數值是 3。