PE文件結構

參考《加密與解密》《Windows PE權威指南》


目錄

1、PE文件的結構

1、什麼是可執行文件?

2、PE文件的特徵

3、PE文件的整體結構

4、PE文件到內存的映射

5、DOS部分

6、PE文件頭(PE Header)

7、塊表

8、RVA與FOA的轉換

2、輸出表和輸入表

1、輸出表(導出表)

2、輸入表(導入表)

3、重定位表

4、資源


1、PE文件的結構

1、什麼是可執行文件?

可執行文件 (executable file) 指的是可以由操作系統進行加載執行的文件。

可執行文件的格式:

    - Windows平臺:PE(Portable Executable)文件結構

    - Linux平臺:ELF(Executable and Linking Format)文件結構

PE和ELF非常相似,它們都是源於同一種可執行文件格式 COFF

    - COFF 是由Unix System V Release 3首先提出並且使用的格式規範,

    - 微軟基於COFF格式,制定了PE格式標準,並將其用於當時的Windows NT系統

    - System V Release 4在COFF的基礎上引入了ELF格式。 

事實上,在Windows平臺,VISUAL C++編譯器產生的目標文件仍然使用COFF格式,而可執行文件爲PE格式

微軟對64位Windows平臺上的PE文件結構叫做PE32+,就是把那些原來32位的字段變成了64位。

2、PE文件的特徵

識別一個文件是不是PE文件不應該只看文件後綴名,還應該通過PE指紋

使用UE打開一個exe文件,發現文件的頭兩個字節都是MZ0x3C位置保存着一個地址,查該地址處發現保存着“PE”,這樣基本可以認定改文件是一個PE文件

通過這些重要的信息(“MZ”和“PE”)驗證文件是否爲PE文件,這些信息即PE指紋。

3、PE文件的整體結構

這裏將一個PE文件的主要部分列爲4部分,這裏可以先有模糊概念,後面會詳細解釋

“節”或“塊”或”區塊“都是一個意思,後文會穿插使用

下面從二進制層面整體把握其結構,看看一個PE文件的組成

4、PE文件到內存的映射

PE文件存儲在磁盤時的結構和加載到內存後的結構有所不同。

當PE文件通過Windows加載器載入內存後,內存中的版本稱爲模塊(Module)

映射文件的起始地址稱爲模塊句柄(hModule),也稱爲基地址(ImageBase)

(模塊句柄是不是和其他句柄不太一樣呢?)

文件數據一般512字節(1扇區)對齊(現也多4k),32位內存一般4k(1頁)對齊,512D = 200H,4096D = 1000H

文件中塊的大小爲200H的整數倍,內存中塊的大小爲1000H的整數倍,映射後實際數據的大小不變,多餘部分可用0填充

PE文件頭部(DOS頭+PE頭)到塊表之間沒有間隙,然而他們卻和塊之間有間隙,大小取決於對齊參數

VC編譯器默認編譯時,exe文件基地址是0x400000,DLL文件基地址是0x10000000 

VA:虛擬內存地址

RVA:相對虛擬地址即相對於基地址的偏移地址

FOA: 文件偏移地址

5、DOS部分

DOS MZ文件頭實際是一個結構體(IMAGE_DOS_HEADER),佔64字節

  1. typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
  2. WORD e_magic; // Magic number
  3. WORD e_cblp; // Bytes on last page of file
  4. WORD e_cp; // Pages in file
  5. WORD e_crlc; // Relocations
  6. WORD e_cparhdr; // Size of header in paragraphs
  7. WORD e_minalloc; // Minimum extra paragraphs needed
  8. WORD e_maxalloc; // Maximum extra paragraphs needed
  9. WORD e_ss; // Initial (relative) SS value
  10. WORD e_sp; // Initial SP value
  11. WORD e_csum; // Checksum
  12. WORD e_ip; // Initial IP value
  13. WORD e_cs; // Initial (relative) CS value
  14. WORD e_lfarlc; // File address of relocation table
  15. WORD e_ovno; // Overlay number
  16. WORD e_res[4]; // Reserved words
  17. WORD e_oemid; // OEM identifier (for e_oeminfo)
  18. WORD e_oeminfo; // OEM information; e_oemid specific
  19. WORD e_res2[10]; // Reserved words
  20. LONG e_lfanew; // File address of new exe header
  21. } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS頭用於16位系統中,在32位系統中DOS頭成爲冗餘數據,但還存在兩個重要成員e_magic字段(偏移 0x0)和                            e_lfanew字段(偏移 0x3C

e_magic保存“MZ”字符,e_lfanew保存PE文件頭地址,通過這個地址找到PE文件頭,得到PE文件標識“PE”。

e_magic和e_lfanew是驗證PE指紋的重要字段,其他字段現基本不使用(可填充任意數據)

“DOS Stub”區域的數據由鏈接器填充(可自己填充如意數據),是一段可以在DOS下運行的一小段代碼,

這段代碼的唯一作用是向終端輸出一行字:“This program cannot be run in DOS”(“e_cs”和“e_ip”指向)

然後退出程序,表示該程序不能在DOS下運行。

 

6、PE文件頭(PE Header)

PE文件頭是一個結構體(IMAGE_NT_HEADERS32),裏面還包含兩個其它結構體,佔用4B + 20B + 224B 

  1. typedef struct _IMAGE_NT_HEADERS {
  2. DWORD Signature; // PE文件標識 4Bytes
  3. IMAGE_FILE_HEADER FileHeader; // 40 Bytes
  4. IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 224 Bytes PE32可執行文件,不討論PE32+的情況
  5. } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Signature字段設置爲0x00004550,ANCII碼字符是“PE00”,標識PE文件頭的開始,PE標識不能破壞。

1、IMAGE_FILE_HEADER結構體

IMAGE_FILE_HEADER(映像文件頭或標準PE頭)結構包含PE文件的一些基本信息,該結構在微軟的官方文檔中被稱爲標準通用對象文件格式(Common Object File Format,COFF)頭

  1. typedef struct _IMAGE_FILE_HEADER {
  2. WORD Machine; // 可運行在什麼樣的CPU上。0代表任意,Intel 386及後續:0x014C, x64: 0x8664
  3. WORD NumberOfSections; // 文件的區塊(節)數
  4. DWORD TimeDateStamp; // 文件的創建時間。1970年1月1日以GMT計算的秒數,編譯器填充的,不重要的值
  5. DWORD PointerToSymbolTable; // 指向符號表(用於調試)
  6. DWORD NumberOfSymbols; // 符號表中符號的個數(用於調試)
  7. WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32結構的大小,可改變,32位爲E0,64位爲F0
  8. WORD Characteristics; // 文件屬性
  9. } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 重要字段:NumberOfSections,SizeOfOptionalHeader

對應結構爲下圖紫線部分

0x014C說明運行於x86 CPU;0x0007說明當前exe有7個節;

0x00E0說明IMAGE_OPTIONAL_HEADER32爲224字節;

0x030F(0000 0011 0000 1111)代表文件屬性 ,由下列對應位爲1的組合

2、IMAGE_OPTIONAL_HEADER結構體

IMAGE_OPTIONAL_HEADER(可選映像頭或擴展PE頭)是一個可選的結構,是IMAGE_FILE_HEADER結構的擴展

大小由IMAGE_FILE_HEADER結構的SizeOfOptionalHeader字段記錄(可能不準確)

  1. typedef struct _IMAGE_OPTIONAL_HEADER {
  2. //
  3. // Standard fields.
  4. //
  5. WORD Magic; //說明文件的類型 PE32:10BH PE32+:20BH Rom映像文件:107H
  6. BYTE MajorLinkerVersion; //鏈接器主版本號
  7. BYTE MinorLinkerVersion; //鏈接器次版本號
  8. DWORD SizeOfCode; //所有代碼節的總和(基於文件對齊) 編譯器填的 沒用
  9. DWORD SizeOfInitializedData; //包含所有已經初始化數據的節的總大小 編譯器填的 沒用
  10. DWORD SizeOfUninitializedData;//包含未初始化數據的節的總大小 編譯器填的 沒用
  11. DWORD AddressOfEntryPoint; //程序入口RVA 在大多數可執行文件中,這個地址不直接指向Main、WinMain或DIMain函數,而指向運行時的庫代碼並由它來調用上述函數
  12. DWORD BaseOfCode; //代碼起始RVA,編譯器填的 沒用
  13. DWORD BaseOfData; //數據段起始RVA,編譯器填的 沒用
  14. //
  15. // NT additional fields.
  16. //
  17. DWORD ImageBase; //內存鏡像基址 ,可鏈接時自己設置
  18. DWORD SectionAlignment; //內存對齊 一般一頁大小4k
  19. DWORD FileAlignment; //文件對齊 一般一扇區大小512字節,現在也多4k
  20. WORD MajorOperatingSystemVersion; //標識操作系統版本號 主版本號
  21. WORD MinorOperatingSystemVersion; //標識操作系統版本號 次版本號
  22. WORD MajorImageVersion; //PE文件自身的主版本號
  23. WORD MinorImageVersion; //PE文件自身的次版本號
  24. WORD MajorSubsystemVersion; //運行所需子系統主版本號
  25. WORD MinorSubsystemVersion; //運行所需子系統次版本號
  26. DWORD Win32VersionValue; //子系統版本的值,必須爲0
  27. DWORD SizeOfImage; //內存中整個PE文件的映射的尺寸,可比實際的值大,必須是SectionAlignment的整數倍
  28. DWORD SizeOfHeaders; //所有頭+節表按照文件對齊後的大小,否則加載會出錯
  29. DWORD CheckSum; //校驗和,一些系統文件有要求.用來判斷文件是否被修改
  30. WORD Subsystem; //子系統 驅動程序(1) 圖形界面(2) 控制檯、DLL(3)
  31. WORD DllCharacteristics; //文件特性 不是針對DLL文件的
  32. DWORD SizeOfStackReserve; //初始化時保留的棧大小
  33. DWORD SizeOfStackCommit; //初始化時實際提交的大小
  34. DWORD SizeOfHeapReserve; //初始化時保留的堆大小
  35. DWORD SizeOfHeapCommit; //初始化時保留的堆大小
  36. DWORD LoaderFlags;
  37. DWORD NumberOfRvaAndSizes; //數據目錄項數目
  38. IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //數據目錄表
  39. } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

重要字段:

AddressOfEntryPoint:程序入口地址(RVA),下圖爲32C40H

ImageBase:內存鏡像基地址,下圖爲400000H

FileAlignment:文件對齊,下圖爲200H

SectionAlignment:內存對齊,下圖爲1000H

DataDirectory[16]:數據目錄表,由數個相同的IMAGE_DATA_DIRECTORY結構組成,

                                指向輸出表、輸入表、資源塊,重定位表等(後面詳解這裏先跳過)

  1. typedef struct _IMAGE_DATA_DIRECTORY {
  2. DWORD VirtualAddress; //對應表的起始RVA
  3. DWORD Size; //對應表長度
  4. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

ImageBase + AddressOfEntryPoint = 程序實際運行入口地址(實際加載地址等於ImageBase)

0x400000 + 0x32C40 = 0x432C40 (使用OD運行程序發現就是從這個地址開始運行)

 

應用:在PE文件空白區添加代碼,讓程序執行先執行添加的代碼再跳轉程序入口

思路:

① 在PE的空白區構造一段代碼(call  -> E8)

② 修改入口地址爲新增代碼(IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint)

③ 新增代碼執行後,跳回入口地址(jmp   -> E9)

 

7、塊表

塊表是一個IMAGE_SECTION_HEADER的結構數組,每個IMAGE_SECTION_HEADER結構40字節。

每個IMAGE_SECTION_HEADER結構包含了它所關聯的區塊的信息,例如位置、長度、屬性。

  1. #define IMAGE_SIZEOF_SHORT_NAME 8
  2. typedef struct _IMAGE_SECTION_HEADER {
  3. BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //塊名。多數塊名以一個“.”開始(例如.text),這個“.”不是必需的
  4. union {
  5. DWORD PhysicalAddress; //常用第二個字段
  6. DWORD VirtualSize; //加載到內存實際區塊的大小(對齊前),爲什麼會變呢?可能是有時未初始化的全局變量不放bss段而是通過擴展這裏
  7. } Misc;
  8. DWORD VirtualAddress; //該塊裝載到內存中的RVA(內存對齊後,數值總是SectionAlignment的整數倍)
  9. DWORD SizeOfRawData; //該塊在文件中所佔的空間(文件對齊後),VirtualSize的值可能會比SizeOfRawData大 例如bss節(SizeOfRawData爲0),data節(關鍵看未初始化的變量放哪)
  10. DWORD PointerToRawData; //該塊在文件中的偏移(FOA)
  11. /*調試相關,忽略*/
  12. DWORD PointerToRelocations; //在“.obj”文件中使用,指向重定位表的指針
  13. DWORD PointerToLinenumbers;
  14. WORD NumberOfRelocations; //重定位表的個數(在OBJ文件中使用)。
  15. WORD NumberOfLinenumbers;
  16. DWORD Characteristics; //塊的屬性 該字段是一組指出塊屬性(例如代碼/數據、可讀/可寫等)的標誌
  17. } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

重要字段:Name[8],VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics

IMAGE_FILE_HEADER的NumberOfSections字段是不是記錄着當前文件的節數呢?

31C80H代表載入內存代碼塊對齊前大小;1000H代表代碼塊裝載到內存RVA1000H;

31E00H代表文件對齊後代碼塊大小;400H代表代碼塊在文件中的偏移

60000020H代表代碼塊屬性(‭0110 0000 0000 0000 0000 0000 0010 0000‬)查下表得到屬性爲可讀可執行的代碼

更多屬性參考:https://docs.microsoft.com/zh-cn/windows/win32/api/winnt/ns-winnt-image_section_header 

 

8、RVA與FOA的轉換

RVA:相對虛擬地址,FOA:文件偏移地址。

計算步驟:

① 計算RVA = 虛擬內存地址 - ImageBase

② 若RVA是否位於PE頭:FOA == RVA

③ 判斷RVA位於哪個節:

  RVA >= 節.VirtualAddress (節在內存對齊後RVA )

  RVA <= .VirtualAddress + 當前節內存對齊後的大小

  偏移量 = RVA - .VirtualAddress;

④ FOA = 節.PointerToRawData + 偏移量;

應用舉例:

有初始值的全局變量初始值會存儲在PE文件中,想要修改文件中全局變量的數據值即

需要找到文件中存儲全局變量值的地方,然後修改即可

 

 

2、輸出表和輸入表

可選PE頭(擴展PE頭)的最後一個字段DataDirectory[16]代表數據目錄表,由16個相同的IMAGE_DATA_DIRECTORY結構組成,成員分別指向輸出表、輸入表、資源塊等

  1. typedef struct _IMAGE_DATA_DIRECTORY {
  2. DWORD VirtualAddress; //對應表的起始RVA
  3. DWORD Size; //對應表大小(包含子表)
  4. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 

1、輸出表(導出表)

創建一個DLL時,實際上創建了一組能讓EXE或其他DLL調用的函數

DLL文件通過輸出表(Export Table)向系統提供輸出函數名、序號和入口地址等信息。

數據目錄表的第1個成員指向輸出表。

 

找到文件中的輸出表(以DllDemo.dll爲例,看圖就行)

成功找到輸出表在文件偏移0C00H處,如下:

特別說明:① 如果文件對齊與內存對齊都是4k則不需要地址轉換 ② 輸出表大小是指輸出表大小與其子表大小和

輸出表實際是一個40字節的結構體(IMAGE_EXPORT_DIRECTORY),輸出表的結構如下

  1. typedef struct _IMAGE_EXPORT_DIRECTORY {
  2. DWORD Characteristics; //未定義,總是爲0。
  3. DWORD TimeDateStamp; //輸出表創建的時間(GMT時間)
  4. WORD MajorVersion; //輸出表的主版本號。未使用,設置爲0。
  5. WORD MinorVersion; //輸出表的次版本號。未使用,設置爲0。
  6. DWORD Name; //指向一個ASCII字符串的RVA。這個字符串是與這些輸出函數相關聯的DLL的名字(例如"KERNEL32.DLL")
  7. DWORD Base; //導出函數起始序號(基數)。當通過序數來查詢一個輸出函數時,這個值從序數裏被減去,其結果將作爲進入輸出地址表(EAT)的索引
  8. DWORD NumberOfFunctions; //輸出函數地址表(Export Address Table,EAT)中的條目數量(最大序號 - 最小序號)
  9. DWORD NumberOfNames; //輸出函數名稱表(Export Names Table,ENT)裏的條目數量
  10. DWORD AddressOfFunctions; // EAT的RVA(輸出函數地址表RVA)
  11. DWORD AddressOfNames; // ENT的RVA(輸出函數名稱表RVA),每一個表成員指向ANCII字符串 表成員的排列順序取決於字符串的排序
  12. DWORD AddressOfNameOrdinals; // 輸出函數序號表RVA,每個表成員2字節
  13. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

重要字段: Name,Base,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals

過程分析:

  1. //功能:加載動態鏈接庫到內存
  2. HMODULE WINAPI LoadLibrary(
  3. LPCTSTR lpFileName //模塊的文件名
  4. );
  1. /*功能:檢索指定的動態鏈接庫(DLL)中的輸出庫函數地址*/
  2. FARPROC GetProcAddress(
  3. HMODULE hModule, // DLL模塊句柄 (模塊基地址)
  4. LPCSTR lpProcName // 函數名 或者 指定函數的序數值
  5. );

PE裝載器調用GetProcAddress來查找DlIDemo.DLL裏的API函數MsgBox,

系統通過定位DlIDemo.DLL的輸出表(IMAGE_EXPORT_DIRECTORY)結構獲得輸出函數名稱表(ENT)的起始地址,

對名字進行二進制查找,直到發現字符串“MsgBox”爲止,PE裝載器發現MsgBox是數組的第1個條目後,加載器從輸出序數表

中讀取相應的第1個值,這個值是MsgBox的在函數地址表(EAT)的索引。使用索引在EAT取值得到MsgBox的RVA1008h

用1008h加DllDemo.DLL的載入地址,得到MsgBox的實際地址。

特別說明:如果lpProcName 是序號,則需要通過字段Base確定起始序號,序號 - Base的差值作爲索引得到函數RVA地址(注意這裏的序號和索引)

注意:輸出序號表存放的是索引值而不是序號,真正的序號是Base+索引值

例如:寫一個簡單加法函數(int add(int a, int b)),創建一個A.dll

  1. //def文件
  2. EXPORTS 
  3. add @12

分析A.dll的導出表 

當時用序號(12)獲得函數地址時會拿12-Base = 0作爲輸出函數地址表的索引值

使用A.dll

  1. #include <iostream>
  2. #include <windows.h>
  3. using namespace std;
  4. typedef int(*lpAdd)(int, int);
  5. lpAdd myAdd;
  6. int main()
  7. {
  8. //動態加載dll到內存中
  9. HINSTANCE hModule = LoadLibrary("A.dll");
  10. cout << "ImageBase: " << hModule << endl;
  11. //通過函數名獲取函數地址
  12. myAdd = (lpAdd)GetProcAddress(hModule, "add");
  13. cout << "myAdd(10, 20) = " << myAdd(10, 20) << endl;
  14. //通過序號獲取函數地址
  15. myAdd = (lpAdd)GetProcAddress(hModule, (char*)0x0C);
  16. cout << "myAdd(10, 20) = " << myAdd(10, 20) << endl;
  17. FreeLibrary(hModule);
  18. return 0;
  19. }

 

 

2、輸入表(導入表)

PE 文件映射到內存後,Windows 將相應的 DLL文件裝入,EXE 文件通過“輸入表”找到相應的 DLL 中的導入函數,從而完成程序的正常運行

數據目錄表的第2個成員指向輸入表。當前文件依賴幾個模塊就會有幾張輸入表且是連續排放的。

如何找到輸入表?

上圖看出當前文件只依賴一個模塊,只有一張導入表,如果有多個會連續存放直到連續出現20個0說明結束。

 

輸入表實際是個20字節的結構體 IMAGE_IMPORT_DESCRIPTOR

  1. typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  2. union {
  3. DWORD Characteristics; // 0 for terminating null import descriptor
  4. DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
  5. } DUMMYUNIONNAME;
  6. DWORD TimeDateStamp; // 0 if not bound,
  7. // -1 if bound, and real date\time stamp
  8. // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
  9. // O.W. date/time stamp of DLL bound to (Old BIND)
  10. DWORD ForwarderChain; // -1 if no forwarders
  11. DWORD Name;
  12. DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
  13. } IMAGE_IMPORT_DESCRIPTOR;

重要字段:

Name:DLL(依賴模塊)名字的指針。是一個以“00”結尾的ASCII字符的RVA地址。

OriginalFirstThunk:包含指向輸入名稱表(INT)的RVA。

      INT是一個IMAGE_THUNK_DATA結構的數組,數組中的每個IMAGE_THUNK_DATA結構都指向

      IMAGE_IMPORT_BY_NAME結構,數組以一個內容爲0的IMAGE_THUNK_DATA結構結束。

FirstThunk:包含指向輸入地址表(IAT)的RVA。IAT是一個IMAGE_THUNK_DATA結構的數組

 

IMAGE_THUNK_DATA結構實際只佔4字節

  1. typedef struct _IMAGE_THUNK_DATA32 {
  2. union {
  3. DWORD ForwarderString; // 指向一個轉向者字符串的RVA
  4. DWORD Function; // 被輸入的函數的內存地址
  5. DWORD Ordinal; // 被輸入的API的序數
  6. DWORD AddressOfData; // 指向IMAGE_IMPORT BY NAME
  7. } u1;
  8. } IMAGE_THUNK_DATA32;

如果IMAGE_THUNK_DATA32的最高位爲1,則低31位代表函數的導出序號,

否則4個字節是一個RVA,指向IMAGE_IMPORT_BY_NAME結構

IMAGE_IMPORT_BY_NAME結構字面僅有4個字節,存儲了一個輸入函數的相關信息

  1. typedef struct _IMAGE_IMPORT_BY_NAME {
  2. WORD Hint; // 輸出函數地址表的索引(不是導出序號),(究竟是啥沒試驗,因爲看的很多資料說是序號),不必須,鏈接器可能將其置0
  3. CHAR Name[1]; // 函數名字字符串,以“\0”作爲字符串結束標誌,大小不確定
  4. } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

由上圖,我們是不是通過導入表能夠很輕鬆獲得當前文件依賴模塊的名字和函數名

這裏INT和IAT完全內容一致,爲什麼呢?稍後解釋

INT和IAT內容一致其實是PE文件未加載時的狀態,

PE加載器將文件載入內存後會向IAT填入真正的函數地址(GetProcAddress)

例如: 

 

3、重定位表

如果PE文件不在首選的地址(ImageBase)載入,那麼文件中的每一個絕對地址都需要被修正。

需要修正的地址有很多,可以在文件中使用重定位表記錄這些絕對地址的位置,在載入內存後若載入基地址與ImageBase不同再進行修正,若相同就不需要修正這些地址。

數據目錄項的第6個結構,指向重定位表(Relocation Table)

重定位表由一個個的重定位塊組成,每個塊記錄了4KB(一頁)的內存中需要重定位的地址

每個重定位數據塊的大小必須以DWORD(4字節)對齊。它們以一個IMAGE_BASE_RELOCATION結構開始,格式如下

  1. typedef struct _IMAGE_BASE_RELOCATION {
  2. DWORD VirtualAddress; //記錄內存頁的基址RVA
  3. DWORD SizeOfBlock; //當前重定位塊結構的大小。這個值減8就是TypeOffset數組的大小
  4. /*下面字段可加與不加*/
  5. /*數組每項大小爲2字節。代表頁內偏移,16位分爲高4位和低12位。高4位代表重定位類型;
  6. 低12位是重定位地址(12位就可以尋址4k),與VitualAddress相加就是一個完整RVA
  7. */
  8. //WORD TypeOffset[1];
  9. } IMAGE_BASE_RELOCATION;
  10. typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

這些字段可能直接不好理解在後面會看一個實例一切就徹底明白了 

雖然有多種重定位類型,但對x86可執行文件來說,所有的基址重定位類型都是IMAGE_REL_BASED_HIGHLOW

在一組重定位結束的地方會出現一個類型IMAGE_REL_BASED_ABSOLUTE的重定位,這些重定位什麼都不做,只用於填充,以便下一個MAGE_BASE_RELOCATION按4字節分界線對齊。

對於IA-64可執行文件,重定位類型似乎總是IMAGE_REL_BASED_DIR64。

有趣的是,儘管IA-64的EXE頁大小是8KB,但基址重定位仍是4KB的塊

所有重定位塊以一個VitualAddress字段爲0的MAGE_BASE_RELOCATION結構結束

  1. //
  2. // Based relocation types.
  3. //
  4. #define IMAGE_REL_BASED_ABSOLUTE 0 // 沒有具體含義,只是爲了讓每個段4字節對齊
  5. #define IMAGE_REL_BASED_HIGH 1
  6. #define IMAGE_REL_BASED_LOW 2
  7. #define IMAGE_REL_BASED_HIGHLOW 3 // 重定位指向的整個地址都需要修正,實際上大部分情況下都是這樣的
  8. #define IMAGE_REL_BASED_HIGHADJ 4
  9. #define IMAGE_REL_BASED_MACHINE_SPECIFIC_5 5
  10. #define IMAGE_REL_BASED_RESERVED 6
  11. #define IMAGE_REL_BASED_MACHINE_SPECIFIC_7 7
  12. #define IMAGE_REL_BASED_MACHINE_SPECIFIC_8 8
  13. #define IMAGE_REL_BASED_MACHINE_SPECIFIC_9 9
  14. #define IMAGE_REL_BASED_DIR64 10 // 出現在64位PE文件中,對指向的整個地址進行修正

示例分析:

繼續以DllDemo.dll爲例

先用工具定位重定位表在文件的位置如下

查看重定位表信息如下

  1. ->Relocation Directory
  2. 1. Relocation Block:
  3. VirtualAddress: 0x00001000 ("CODE")
  4. SizeOfBlock: 0x00000010 (0x0004 block entries)
  5. RVA Type
  6. ---------- -----------------
  7. 0x0000100F HIGHLOW
  8. 0x00001023 HIGHLOW
  9. n/a ABSOLUTE
  10. n/a ABSOLUTE

下面實際分析

根據下面判斷出當前RVA在CODE節

所以

        100Fh(RVA)  →   60Fh(FOA)

        1023h(RVA)  →   623h(FOA)

60Fh和623h分別指向00402000h和00403030h處,即爲所需要重定位的數據

執行PE文件前,加載程序在進行重定位的時候,會用PE文件在內存中的實際映像地址減PE文件所要求的映像地址,根據重定位類型的不同將差值添加到相應的地址數據中

可以看到重定位表扮演的角色:文件加載到內存後,通過重定位表記錄的RVA找到需要重定位的數據

重定位表通過頁基址RVA+頁內偏移地址方式得到一個完整RVA大大縮小了表大小。

 

4、資源

Windows程序的各種界面稱爲資源,包括加速鍵(Accelerator)、位圖(Bitmap)、光標(Cursor)、對話框(Dialog Box)、圖標(Icon)、菜單(Menu)、串表(String Table)、工具欄(Toolbar)和版本信息(Version Information)等。

定義資源時,既可以使用字符串作爲名稱來標識一個資源,也可以通過ID號來標識資源

資源分類

     - 標準資源類型

     - 非標準資源類型

               若資源類型的高位如果爲1,說明對應的資源類別是一個非標準的新類型


數據目錄項的第3個結構,指向資源表,不直接指向資源數據,而是以磁盤目錄形式定位資源數據

資源表是一個四層的二叉排序樹結構。

每一個節點都是由資源目錄結構和緊隨其後的數個資源目錄項結構組成的,

兩種結構組成了一個資源目錄結構單元(目錄塊)

 

資源目錄結構(IMAGE_RESOURCE_DIRECTORY)佔16字節,其定義如下

  1. typedef struct _IMAGE_RESOURCE_DIRECTORY {
  2. DWORD Characteristics; //理論上是資源的屬性標誌,但是通常爲0
  3. DWORD TimeDateStamp; //資源建立的時間
  4. WORD MajorVersion; //理論上是放置資源的版本,但是通常爲0
  5. WORD MinorVersion;
  6. //定義資源時,既可以使用字符串作爲名稱來標識一個資源,也可以通過ID號來標識資源。資源目錄項的數量等於兩者之和。
  7. WORD NumberOfNamedEntries; //以字符串命名的資源數量
  8. WORD NumberOfIdEntries; //以整型數字(ID)命名的資源數量
  9. // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
  10. } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

資源目錄項結構(IMAGE_RESOURCE_DIRECTORY_ENTRY),佔8字節,包含2個字段,結構定義如下。

  1. //如果看不懂下面的結構建議複習一下C中的union,struct,位域
  2. typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
  3. union {
  4. struct {
  5. DWORD NameOffset:31;
  6. DWORD NameIsString:1;
  7. } DUMMYSTRUCTNAME;
  8. DWORD Name;
  9. WORD Id;
  10. } DUMMYUNIONNAME;
  11. union {
  12. DWORD OffsetToData;
  13. struct {
  14. DWORD OffsetToDirectory:31;
  15. DWORD DataIsDirectory:1;
  16. } DUMMYSTRUCTNAME2;
  17. } DUMMYUNIONNAME2;
  18. } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

重要字段:

Name字段:定義目錄項的名稱或ID。

     - 當結構用於第1層目錄時,定義的是資源類型

     - 當結構用於第2層目錄時,定義的是資源的名稱

     - 當結構用於第3層目錄時,定義的是代碼頁編號

     - 當最高位爲0時,表示字段的值作爲ID使用;由該字段的低16位組成整數標識符ID

     - 當最高位爲1時,表示字段的低位作爲指針使用,資源名稱字符串使用Unicode編碼,

       這個指針不直接指向字符串,而指向一個IMAGE_RESOURCE_DIR_STRING_U結構。

  1. typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
  2. WORD Length; //字符串的長度
  3. WCHAR NameString[ 1 ]; //Unicode字符串,按字對齊,長度可變;由Length 指明 Unicode字符串的長度
  4. } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

 OffsetToData字段:是一個指針。

     - 當最高位(位31)爲1時,低位數據指向下一層目錄塊的起始地址;

     - 當最高位爲0時,指針指向IMAGE_RESOURCE_DATA_ENTRY結構。

第3層目錄結構中的OffsetToData將指向IMAGE_RESOURCE_DATA_ENTRY結構。

該結構描述了資源數據的位置和大小,其定義如下。

  1. typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
  2. DWORD OffsetToData; //資源數據的RVA
  3. DWORD Size; //資源數據的長度
  4. DWORD CodePage; //代碼頁,一般爲0
  5. DWORD Reserved; //保留字段
  6. } IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

重要字段:

OffsetToData:指向資源數據的指針(RVA)

Size:資源數據的長度

 

實例分析:

定位資源在文件中的位置

                      由於當前exe文件對齊與內存對齊都是4k,RVA不需要轉FOA

 

所以:

圖標的真正資源數據RVA爲4100h,大小爲2E8h。

菜單的真正資源數據RVA爲4400h,大小爲5Ah。

圖標組的真正資源數據RVA爲43E8h,大小爲14h。

 

使用工具驗證

可以清晰看到根目錄有3個資源目錄項(Icon,Menu,Icon Group)

第二層爲資源ID或資源名稱

第三層爲代碼頁ID爲2052表簡體中文,1033表美國英語

右下角圖標爲真正資源數據


更多內容將來可能添加

 

 

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