1. 程式人生 > 程式設計 >PE檔案結構詳解

PE檔案結構詳解

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檔案,發現檔案的頭兩個位元組都是MZ,0x3C位置儲存著一個地址,查該地址處發現儲存著“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位元組

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;

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

typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature;             // PE檔案標識 4Bytes
  IMAGE_FILE_HEADER FileHeader;      // 40 Bytes
  IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 224 Bytes  PE32可執行檔案,不討論PE32+的情況
} 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)頭

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;        // 可執行在什麼樣的CPU上。0代表任意,Intel 386及後續:0x014C, x64: 0x8664
  WORD  NumberOfSections;   // 檔案的區塊(節)數
  DWORD  TimeDateStamp;     // 檔案的建立時間。1970年1月1日以GMT計算的秒數,編譯器填充的,不重要的值
  DWORD  PointerToSymbolTable; // 指向符號表(用於除錯)
  DWORD  NumberOfSymbols;    // 符號表中符號的個數(用於除錯)
  WORD  SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32結構的大小,可改變,32位為E0,64位為F0
  WORD  Characteristics;    // 檔案屬性
} 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欄位記錄(可能不準確)

typedef struct _IMAGE_OPTIONAL_HEADER {
  //
  // Standard fields.
  //
  
  WORD  Magic;         //說明檔案的型別 PE32:10BH PE32+:20BH  Rom映像檔案:107H
  BYTE  MajorLinkerVersion;   //連結器主版本號
  BYTE  MinorLinkerVersion;   //連結器次版本號
  DWORD  SizeOfCode;       //所有程式碼節的總和(基於檔案對齊) 編譯器填的 沒用
  DWORD  SizeOfInitializedData; //包含所有已經初始化資料的節的總大小 編譯器填的 沒用
  DWORD  SizeOfUninitializedData;//包含未初始化資料的節的總大小 編譯器填的 沒用
 
  DWORD  AddressOfEntryPoint;  //程式入口RVA  在大多數可執行檔案中,這個地址不直接指向Main、WinMain或DIMain函式,而指向執行時的庫程式碼並由它來呼叫上述函式
  DWORD  BaseOfCode;       //程式碼起始RVA,編譯器填的  沒用
  DWORD  BaseOfData;       //資料段起始RVA,編譯器填的  沒用
 
  //
  // NT additional fields.
  //
 
  DWORD  ImageBase;       //記憶體映象基址 ,可連結時自己設定
  DWORD  SectionAlignment;    //記憶體對齊   一般一頁大小4k
  DWORD  FileAlignment;     //檔案對齊   一般一扇區大小512位元組,現在也多4k
  WORD  MajorOperatingSystemVersion; //標識作業系統版本號 主版本號
  WORD  MinorOperatingSystemVersion; //標識作業系統版本號 次版本號
  WORD  MajorImageVersion;   //PE檔案自身的主版本號 
  WORD  MinorImageVersion;   //PE檔案自身的次版本號 
  WORD  MajorSubsystemVersion; //執行所需子系統主版本號
  WORD  MinorSubsystemVersion; //執行所需子系統次版本號
  DWORD  Win32VersionValue;   //子系統版本的值,必須為0
  DWORD  SizeOfImage; //記憶體中整個PE檔案的對映的尺寸,可比實際的值大,必須是SectionAlignment的整數倍  
  DWORD  SizeOfHeaders;     //所有頭+節表按照檔案對齊後的大小,否則載入會出錯
  DWORD  CheckSum;        //校驗和,一些系統檔案有要求.用來判斷檔案是否被修改 
  WORD  Subsystem;       //子系統	驅動程式(1) 圖形介面(2) 控制檯、DLL(3)
  WORD  DllCharacteristics;   //檔案特性 不是針對DLL檔案的
  DWORD  SizeOfStackReserve;   //初始化時保留的棧大小 
  DWORD  SizeOfStackCommit;   //初始化時實際提交的大小 
  DWORD  SizeOfHeapReserve;   //初始化時保留的堆大小
  DWORD  SizeOfHeapCommit;    //初始化時保留的堆大小
  DWORD  LoaderFlags; 
  DWORD  NumberOfRvaAndSizes;  //資料目錄項數目
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //資料目錄表
} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

重要欄位:

AddressOfEntryPoint:程式入口地址(RVA),下圖為32C40H

ImageBase:記憶體映象基地址,下圖為400000H

FileAlignment:檔案對齊,下圖為200H

SectionAlignment:記憶體對齊,下圖為1000H

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

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

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD  VirtualAddress;  //對應表的起始RVA
  DWORD  Size;       //對應表長度
} 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結構包含了它所關聯的區塊的資訊,例如位置、長度、屬性。

#define IMAGE_SIZEOF_SHORT_NAME       8
 
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME]; //塊名。多數塊名以一個“.”開始(例如.text),這個“.”不是必需的
  union {
      DWORD  PhysicalAddress; //常用第二個欄位
      DWORD  VirtualSize;   //載入到記憶體實際區塊的大小(對齊前),為什麼會變呢?可能是有時未初始化的全域性變數不放bss段而是通過擴充套件這裡
  } Misc;
  DWORD  VirtualAddress;  //該塊裝載到記憶體中的RVA(記憶體對齊後,數值總是SectionAlignment的整數倍)
  DWORD  SizeOfRawData;  //該塊在檔案中所佔的空間(檔案對齊後),VirtualSize的值可能會比SizeOfRawData大 例如bss節(SizeOfRawData為0),data節(關鍵看未初始化的變數放哪)
  DWORD  PointerToRawData; //該塊在檔案中的偏移(FOA)
 
  /*除錯相關,忽略*/
  DWORD  PointerToRelocations; //在“.obj”檔案中使用,指向重定位表的指標
  DWORD  PointerToLinenumbers;
  WORD  NumberOfRelocations;  //重定位表的個數(在OBJ檔案中使用)。
  WORD  NumberOfLinenumbers;
 
  DWORD  Characteristics; //塊的屬性 該欄位是一組指出塊屬性(例如程式碼/資料、可讀/可寫等)的標誌
} 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結構組成,成員分別指向輸出表、輸入表、資源塊等

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

1、輸出表(匯出表)
建立一個DLL時,實際上建立了一組能讓EXE或其他DLL呼叫的函式

DLL檔案通過輸出表(Export Table)向系統提供輸出函式名、序號和入口地址等資訊。

資料目錄表的第1個成員指向輸出表。

找到檔案中的輸出表(以DllDemo.dll為例,看圖就行)

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

特別說明:① 如果檔案對齊與記憶體對齊都是4k則不需要地址轉換 ② 輸出表大小是指輸出表大小與其子表大小和

輸出表實際是一個40位元組的結構體(IMAGE_EXPORT_DIRECTORY),輸出表的結構如下

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

重要欄位: Name,Base,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals

過程分析:

//功能:載入動態連結庫到記憶體
HMODULE WINAPI LoadLibrary(
LPCTSTR lpFileName //模組的檔名
);

/*功能:檢索指定的動態連結庫(DLL)中的輸出庫函式地址*/
FARPROC GetProcAddress(
HMODULE hModule,// DLL模組控制代碼 (模組基地址)
LPCSTR lpProcName // 函式名 或者 指定函式的序數值
);

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

//def檔案

EXPORTS

add @12

分析A.dll的匯出表

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

使用A.dll

#include <iostream>
#include <windows.h>
 
using namespace std;
 
typedef int(*lpAdd)(int,int);
 
lpAdd myAdd;
 
int main()
{
	//動態載入dll到記憶體中
	HINSTANCE  hModule = LoadLibrary("A.dll");
 
	cout << "ImageBase: " << hModule << endl;
 
	//通過函式名獲取函式地址
	myAdd = (lpAdd)GetProcAddress(hModule,"add");
 
	cout << "myAdd(10,20) = " << myAdd(10,20) << endl;
 
	//通過序號獲取函式地址
	myAdd = (lpAdd)GetProcAddress(hModule,(char*)0x0C);
 
	cout << "myAdd(10,20) << endl;
 
	FreeLibrary(hModule);
 
	return 0;
}

2、輸入表(匯入表)

PE 檔案對映到記憶體後,Windows 將相應的 DLL檔案裝入,EXE 檔案通過“輸入表”找到相應的 DLL 中的匯入函式,從而完成程式的正常執行

資料目錄表的第2個成員指向輸入表。當前檔案依賴幾個模組就會有幾張輸入表且是連續排放的。

如何找到輸入表?

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

輸入表實際是個20位元組的結構體 IMAGE_IMPORT_DESCRIPTOR

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  union {
    DWORD  Characteristics;      // 0 for terminating null import descriptor
    DWORD  OriginalFirstThunk;     // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
  } DUMMYUNIONNAME;
  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
  DWORD  Name;
  DWORD  FirstThunk;           // RVA to IAT (if bound this IAT has actual addresses)
} 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位元組

typedef struct _IMAGE_THUNK_DATA32 {
  union {
    DWORD ForwarderString;   // 指向一個轉向者字串的RVA
    DWORD Function;       // 被輸入的函式的記憶體地址
    DWORD Ordinal;       // 被輸入的API的序數
    DWORD AddressOfData;    // 指向IMAGE_IMPORT BY NAME
  } u1;
} IMAGE_THUNK_DATA32;

如果IMAGE_THUNK_DATA32的最高位為1,則低31位代表函式的匯出序號,

否則4個位元組是一個RVA,指向IMAGE_IMPORT_BY_NAME結構

IMAGE_IMPORT_BY_NAME結構字面僅有4個位元組,儲存了一個輸入函式的相關資訊

typedef struct _IMAGE_IMPORT_BY_NAME {
  WORD  Hint;  // 輸出函式地址表的索引(不是匯出序號),(究竟是啥沒試驗,因為看的很多資料說是序號),不必須,連結器可能將其置0
  CHAR  Name[1]; // 函式名字字串,以“\0”作為字串結束標誌,大小不確定
} 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結構開始,格式如下

typedef struct _IMAGE_BASE_RELOCATION {
  DWORD  VirtualAddress; //記錄記憶體頁的基址RVA 
  DWORD  SizeOfBlock;  //當前重定位塊結構的大小。這個值減8就是TypeOffset陣列的大小
  
  /*下面欄位可加與不加*/
  /*陣列每項大小為2位元組。代表頁內偏移,16位分為高4位和低12位。高4位代表重定位型別;
   低12位是重定位地址(12位就可以定址4k),與VitualAddress相加就是一個完整RVA
   */
  //WORD  TypeOffset[1]; 
} IMAGE_BASE_RELOCATION;
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結構結束。

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

示例分析:

繼續以DllDemo.dll為例

先用工具定位重定位表在檔案的位置如下

檢視重定位表資訊如下

->Relocation Directory
  1. Relocation Block:
  VirtualAddress: 0x00001000 ("CODE")
  SizeOfBlock:   0x00000010 (0x0004 block entries)
 
  RVA    Type
  ---------- -----------------
  0x0000100F HIGHLOW
  0x00001023 HIGHLOW
  n/a    ABSOLUTE
  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位元組,其定義如下

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

資源目錄項結構(IMAGE_RESOURCE_DIRECTORY_ENTRY),佔8位元組,包含2個欄位,結構定義如下。

//如果看不懂下面的結構建議複習一下C中的union,struct,位域
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
  union {
    struct {
      DWORD NameOffset:31;
      DWORD NameIsString:1;
    } DUMMYSTRUCTNAME;
    DWORD  Name;
    WORD  Id;
  } DUMMYUNIONNAME;
  union {
    DWORD  OffsetToData;
    struct {
      DWORD  OffsetToDirectory:31;
      DWORD  DataIsDirectory:1;
    } DUMMYSTRUCTNAME2;
  } DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;

重要欄位:

Name欄位:定義目錄項的名稱或ID。

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

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

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

- 當最高位為0時,表示欄位的值作為ID使用;由該欄位的低16位組成整數識別符號ID

- 當最高位為1時,表示欄位的低位作為指標使用,資源名稱字串使用Unicode編碼,

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

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

OffsetToData欄位:是一個指標。

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

- 當最高位為0時,指標指向IMAGE_RESOURCE_DATA_ENTRY結構。

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

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

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
  DWORD  OffsetToData;  //資源資料的RVA
  DWORD  Size;      //資源資料的長度
  DWORD  CodePage;    //內碼表,一般為0
  DWORD  Reserved;    //保留欄位
} 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表美國英語

右下角圖示為真正資源資料

好了這篇文章暫時就先介紹到這了,希望大家以後多多支援我們。