文章中的圖檔大部分為自己做截屏,少量摘自網絡,若有侵權請及時告知,我會盡快删除。轉載請注明出處。
接上一篇,上一篇了解了PE頭及可選頭的基本結構,這一篇從區段表開始學習。
3.區段表
PE檔案頭後面是區段表,用于描述各個區段的屬性,檔案至少擁有一個區段才能執行。區段表是多個相連的IMAGE_SECTION_HEADER結構組成。
3.1IMAGE_SECTION_HEADER結構
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //區段名,長度8位元組的ASCII字元串
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
}Misc; //區段大小,實際被使用的區段大小,即未被對齊之前的大小
DWORD VirtualAddress; //此區段加載到記憶體後的RVA,按照記憶體頁對齊
DWORD SizeOfRawData; //此區段在磁盤中的體積,按照檔案頁對齊
DWORD PointerToRawData;//此區段在檔案中的偏移
DWORD PointerToRelocations;//此區段重定位表的偏移(用于OBJ檔案)
DWORD PointerToLinenumbers;//行号表在檔案中的偏移(用于調試)
WORD NumberOfRelocations;//重定位表項數量(用于OBJ檔案)
WORD NumberOfLinenumbers;//行号表項數量
DWORD Characteristics;//區段屬性值,具體的值和前面那張檔案屬性表相同
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
3.2區段名功能約定
區段名 | 描述 |
.text | 代碼段,裡面的資料全都是代碼 |
.data | 可讀寫的資料段,存放全局變量或靜态變量 |
.rdata | 隻讀資料區 |
.idata | 導入資料區,存放導入表資訊 |
.edata | 導出資料區,導出表資訊 |
.rsrc | 資源區段,存放程式用到的所有資源,如圖表,菜單等 |
.bss | 未初始化資料區 |
.crt | 用于支援C++運作時庫所添加的資料 |
.tls | 存儲線程局部變量 |
.reloc | 包含重定位資訊 |
.sdata | 包含相對于可被全局指針定位的可讀寫資料 |
.srdata | 包含相對于可被全局指針定位的隻讀資料 |
.pdata | 包含異常表 |
.debug$S | 包含OBJ檔案中的Codeview格式符号 |
.debug$T | 包含OBJ檔案中的Codeview格式類型的符号 |
.debug$P | 包含使用預編譯頭時的一些資訊 |
.drectve | 包含編譯時的一些連結指令 |
.didat | 包含延遲裝入的資料 |
上面表格中空着的都是不常用的。
4.導出表
導出表是PE檔案為其他應用程式提供API的一種函數導出方式
4.1IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //保留,恒為0x00000000
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; // RVAfrom base of image指向導出序列号的數組
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
4.2識别導出表
到處便可以由名稱表、函數表和序号表,後兩者是必須要的,名稱表則是可選的。名稱表和序号表起到索引找到函數表中的函數的作用,而函數表則是記錄函數位址的。然而導出表的序号順序和函數順序并不是對應的,在記憶體中的資料是被完全打亂的,函數的順序是按照名稱表來确定的。
首先需要在資料目錄項中獲得導出表的RVA,然後再觀察所有段的RVA,這時就會知道導出表在所在的段的RVA,相減獲得偏移位址,最後檢視段表獲得該段在檔案中的偏移,最終獲得導出表的偏移位址。公式如下:
導出表Offset = 導出表RVA – 有導出表的段RVA + 該段在檔案中的偏移Offset
這裡把書上的例子dll檔案拿來,導出表截圖如下:

可以根據上面的資料結構獲得每一項的值。比如此導出表的序号從1開始,導出位址表EAT偏移為0x1398,導出名稱表ENT偏移為0x13A8。下面分别是ENT和EAT的截圖:
根據上面的偏移可以知道從0x1398~0x13A7為EAT,後面的0x10位元組為ENT,根據ENT中的位址計算出函數名在檔案中的偏移量是從0x12C8開始;而0x13B8~0x13BF八個位元組則是序号表。
通過上面這張圖很明顯的看到函數名和函數位址的對應關系,但是有一個有點問題,就是最後一個導出函數的位址指向0x00003354,這是在.data段中的,是不可執行的,那為什麼會這樣呢?從ENT指向的對應函數名可以看到他的名字是nPEDemo而不是帶有fn開頭的字元串,其實這是一個導出的全局變量,是以會在.data 段中。
5.導入表
5.1導入表結構
導入表機制是指PE檔案從第三方程式中導入API以提供本函數調用的機制。事實上Windows平台下所有系統提供的API函數都是通過導入導出表完成的。是以想要看程式調用了哪些函數就要看導入表。但是其實也可以手工來調用而不是直接調用,比如用LoadLibrary()等函數加載dll檔案然後獲得裡面函數的指針,最終直接通過位址調用函數。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVAto original unbound INT (PIMAGE_THUNK_DATA)
};//指向輸入名稱表INT的RVA,INT是一個IMAGE_THUNK_DATA數組,而//IMAGE_THUNK_DATA數組又指向IMAGE_IMPORT_BY_NAME,數組的最後是内容為0
//的IMAGE_THUNK_DATA結構
DWORD TimeDateStamp; // 0 if notbound, -1 if bound, and real date\time stamp
DWORD ForwarderChain;//inIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stampof DLL bound to (Old BIND)
//-1 if no forwarders。轉發鍊,為0則不轉發
DWORD Name; //指向導入映像檔案的名字
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actualaddresses)指向輸入位址表IAT的RVA
} IMAGE_IMPORT_DESCRIPTOR;
在32位系統中IMAGE_THUNK_DATA的結構為:
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;//轉發字元串RVA,當上面的轉發字段為0此值有效
PDWORD Function; //被導入函數的實際記憶體位址
DWORD Ordinal; //被導入函數的序号,當IMAGE_THUNK_DATA高位1則有效
PIMAGE_IMPORT_BY_NAME AddressOfData;//指向輸入名稱表,上面3個均無效時有效
}u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 *PIMAGE_THUNK_DATA32;
下面再來看看最後兩個值的意思。Ordinal當最高位為1表示使用序号導入函數,這時低31位就是序号。Function字段是系統所用,在系統加載程式之前先逐個周遊IAT然後從這個字段取出導入函數的記憶體位址,将這些位址逐一跳入對應的IAT。
下面再來看看IMAGE_IMPORT_BY_NAME結構:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //需導入的函數序号
BYTE Name[1];//需導入的函數名稱
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
5.2識别導入表
還是使用上面的那個dll檔案,現在資料目錄表中第二項找到導入表RVA和大小,然後計算在檔案中的偏移:
據此找到導入表及其範圍:
OriginalFirstThunk的RVA偏移0x227C,偏移為0x107C,就跟在導入表後面,這就是IAT。由于IMAGE_IMPORT_DESCRIPTOR長度為0x14,可以看到這裡有三個這樣的結構組成數組表示從3個dll檔案導入了函數,最後由填充了0x00的IMAGE_IMPORT_DESCRIPTOR結構結束。這裡就拿第一個結構為例進行INT和IAT介紹,這裡結構的頭4位元組OriginalFirstThunk指向INT是0x227C,轉化為偏移0x107C:
裡面的值為0x2304,指向的是對應的IMAGE_IMPORT_BY_NAME。再看塊的最後4位元組0x2000,偏移量0xE00:
裡面的值0x2304,也是指向同一個IMAGE_IMPORT_BY_NAME,再來看這個結構:
可以看到前兩位元組0x0421為函數序号,後面的字元串就是函數名。下面這幅圖很好的解釋了導入表的結構:
6.1資源結構
資源在PE檔案中是以三級目錄形式存在的,分别是資源類型、目錄資源ID與資源代碼頁。這三個目錄都是由一個IMAGE_RESOURCE_DIRECTORY為頭部的,并且在後面跟着IMAGE_RESOURCE_DIRECTORY_ENTRY結構數組。頭部為後面的數組指定成員個數,而後面的結構則指向下一層目錄結構(或資源資料)。
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //資源屬性,一般均為0
DWORD TimeDateStamp; //資源建立時間
WORD MajorVersion; //資源的主版本,一般情況下為0x0004
WORD MinorVersion; //資源的子版本,一般情況下為0x0000
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; // NameIsString為1時資源名字元串偏移,其指向一個儲存資源名稱的結構體
DWORD NameIsString:1;//資源名為字元串
};
DWORD Name;//當位于第一層目錄時表示資源類型的值,當位于第三層時,儲存資源語言區域類型值
WORD Id; //資源數字ID
};
union {
DWORD OffsetToData; //資料偏移位址
struct {
DWORD OffsetToDirectory:31;// DataIsDirectory:1時指向下層目錄偏移位址
DWORD DataIsDirectory:1; //資料為目錄
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;
下面是資源類型的值:
上面這個IMAGE_RESOURCE_DIRECTORY_ENTRY結構由兩個大小為4位元組的聯合體組成,不同情況下聯合體的有效字段是不同的:
第一個聯合體由結構所處的目錄層決定,位于第一層目錄則Name字段有效,儲存資源類型;位于第二層去取決于資源引用方式,若是采用編号則ID有效,否則結構體有效;位于第三層Name字段有效,儲存的資訊是資源語言類型。
第二個聯合體内的字段根據具體情況決定,如果下一級是子目錄則結構體有效,否則OffsetToData字段有效。
經過三層目錄之後最終達到一個結構體IMAGE_RESOURCE_DATA_ENTRY,這個結構将指引我們找到資源資料:
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;//資源資料的RVA
DWORD Size; //資源資料長度
DWORD CodePage; //代碼頁資訊
DWORD Reserved; //保留字段,恒為0x00000000
} IMAGE_RESOURCE_DATA_ENTRY,*PIMAGE_RESOURCE_DATA_ENTRY;
下面這個圖簡要描述了資源的結構:
6.2識别資源
還是和上面一樣現在資料目錄表中找到資源在檔案中的偏移,這裡為0x1600。下面還是通過例子來看:
頭0x10位元組是IMAGE_RESOURCE_DIRECTORY,最後兩個位元組是用數字作為ID的項目個數,這裡兩個表示後面有兩個0x08位元組的IMAGE_RESOURCE_DIRECTORY_ENTRY結構體。拿第一個作為例子。由于是第一層目錄,是以頭4位元組是Name字段,表示資源類型為字元串,0x80000020中第一位為1表示DataIsDirectory,即為目錄,後面31位表示偏移,是以這裡二級目錄偏移為0x20,可以得到相對檔案偏移為0x1620,轉到那裡去看:
同樣看到利用編号索引的子項目數為1,是以接下來隻有0x8位元組是第三級目錄。由于使用編号索引,是以前4位元組ID有效為0x07,後4位元組計算的資源偏移為0x1650,到那裡看看:
這裡也是隻有一個用編号索引的目錄,由于是第三層,是以前4位元組Name有效,儲存資源語言區域類型,後4位元組表示資源偏移,是以可以到0x1680處尋找資源結構IMAGE_RESOURCE_DATA_ENTRY:
可以看到資料RVA為0x40A0,長度為0x38,代碼頁資訊為0x04E4。
至此,關于資源的三層目錄簡要介紹就差不多了。
PE檔案的前兩篇主要是講一些重要的結構,後面的一些内容不常用,是以第3篇會講的簡單一點。