1. 程式人生 > >01 PE檔案的兩種狀態

01 PE檔案的兩種狀態

首先,我們回顧一下上篇中的PE檔案的主要結構如下圖;

在這裡插入圖片描述

然後,我們從 細節上來了解一下PE檔案結構;

1.DOS部分

DOS部分包含了兩部分,第一部分是IMAGE_DOS_HEADER結構體,我們稱其為DOS MZ檔案頭,裝了vs2013的話這個結構體在C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include\WinNT.h (注:其他PE結構體也都在這個檔案裡)中定義了,其他版本可以類比目錄結構進行查詢,或者直接在電腦上搜索,其結構體(其大小為64個位元組,可以自己數一下,也可以寫個程式測一下,同樣可以看下面結構體中給的偏移)如下,為了方便使用我們在前面加上了偏移:
在這裡插入圖片描述


下面我們用winhex開啟記事本notepad.exe這個程式來看一下DOS MZ頭部長什麼樣,如下圖:
在這裡插入圖片描述
接下來是DOS塊,這段資料長度是不確定的,主要給連結器用,在Windows下這塊被刪掉也不影響程式執行,雖然大小不固定,但是也有辦法來確定其大小,我們從IMAGE_DOS_HEADER結構體上可以看出,其最後一個成員指向了PE頭部,那麼就可以知道DOS Stub部分就在DOS頭部到PE頭部標記之間,如下圖():
在這裡插入圖片描述

2.PE檔案頭

PE檔案頭由三部分組成,先從結構體來看一下它長啥樣:
在這裡插入圖片描述
我們再看看IMAGE_FILE_HEADER結構體,如下:

在這裡插入圖片描述我們看一下Signature和FileHeader部分的二進位制:
在這裡插入圖片描述


接下來我們看看IMAGE_OPTIONAL_HEADER32結構體,其大小不是固定的,一般32位大小為0x00E0,64位為0x00F0,其結構體如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
0x18h      WORD Magic;			//標誌字,(ROM映像0107h),,普通可執行					//檔案(010Bh)
0x1Ah      BYTE MajorLinkerVersion;		//連線程式主版本號
0x1Bh      BYTE MinorLinkerVersion;		//連線程式福版本號
0x1Ch      DWORD SizeOfCode;			//所有程式碼區塊的總大小
0x20h      DWORD SizeOfInitializedData;		//所有已初始化資料的總大小
0x24h      DWORD SizeOfUninitializedData;		//所有未初始化資料的總大小
0x28h      DWORD AddressOfEntryPoint;		//程式執行入口RVA
0x2Ch      DWORD BaseOfCode;			//程式碼區塊起始RVA
0x30h      DWORD BaseOfData;			//資料區塊起始RVA
//以下屬於NT結構增加的領域
0x34h      DWORD ImageBase;		//程式首選裝載地址
0x38h      DWORD SectionAlignment;		//記憶體中區塊的對齊值大小
0x3Ch      DWORD FileAlignment;		//檔案中區塊的對齊值大小
0x40h      WORD MajorOperatingSystemVersion;	//要求作業系統最低主版本號
0x42h      WORD MinorOperatingSystemVersion;	//要求作業系統的最低福版本號
0x44h      WORD MajorImageVersion;		//映象主版本號
0x46h      WORD MinorImageVersion;		//映象福版本號
0x48h      WORD MajorSubsystemVersion;	//最低子系統主版本號
0x4Ah      WORD MinorSubsystemVersion;	//最低子系統福版本號
0x4Ch      DWORD Win32VersionValue;		//保留,必須為0(沒有被病毒感染時)
0x50h      DWORD SizeOfImage;		//映像裝入記憶體的總尺寸(記憶體對齊					//的倍數)
0x54h      DWORD SizeOfHeaders;		//所有頭加區塊表的大小
0x58h      DWORD CheckSum;			//映像校驗和
0x5Ch      WORD Subsystem;			//可執行檔案期望的子系統
0x5Eh	   WORD DllCharacteristics;	//DllMain何時被呼叫
0x60h      DWORD SizeOfStackReserve;//初始化時棧的大小
0x64h      DWORD SizeOfStackCommit; //初始化時實際提交棧的大小
0x68h      DWORD SizeOfHeapReserve; //初始化時保留的堆大小
0x6Ch      DWORD SizeOfHeapCommit; //初始化時實際提交棧的大小
0x70h      DWORD LoaderFlags;	//與除錯有關,預設為0
0x74h      DWORD NumberOfRvaAndSizes;//下邊資料目錄的項數
0x78h  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//資料目錄表
} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

接下來我們在二進位制中找到它,如下:
在這裡插入圖片描述

3.節表

節表非常重要,我們程式真正的資料存放到一個一個的節中,我們有多少節,每個節從哪裡開始,哪裡結束,裡面存放的是什麼資料,都是由節表來記錄的,同樣我們先來看一下其結構體長什麼樣,如圖:
在這裡插入圖片描述
接下來我們看看二進位制中節表是什麼樣,一個節表40個位元組,如圖:
在這裡插入圖片描述
從上圖可以看到它有五個節。

4.節

那我們找到了前三個部分,而且前三個部分都是連續的,那第四部分的節從哪找呢?這時我們需要了解一個IMAGE_OPTIONAL_HEADER32欄位 SizeOfHeaders,這個欄位存的就是DOS頭部PE頭部和節表大小相加後按檔案對齊值對齊後的大小,比如他們相加是0x354個位元組,但是這個欄位裡存的一定不會是0x354這個資料,因為它是按檔案對齊後的大小。那麼什麼是檔案對齊值?
同樣在擴充套件頭結構中有一個FileAlignment欄位用來存放檔案對齊值,其可能值有可能是0x200或0x1000,如果這個值是0x200這SizeOfHeaders中存的就會是0x400,如果FileAlignment值是0x1000,那麼SizeOfHeaders中存的就會是0x1000,其思想類似C中結構體對齊,以空間換時間。
接下來我們到分析的檔案中找找這個值,FileAlignment由上面給的結構體,我們已經標識出其相對PE標識的偏移值是0x3C,如圖:

在這裡插入圖片描述
從上圖PE標識處偏移三行,再偏移C位元組找到FileAlignment,從其值可以看到,這個值就是0x200(看不懂了解一下大小端),那麼SizeOfHeaders欄位的值一定是0x200的整數倍,就是以0x200的整數倍向上取整,接下來我們找一下SizeOfHeaders的值,從上面結構體中我們可以看出,相對PE標識偏移0x54,如圖:
在這裡插入圖片描述
可以看出其大小時0x400,就是0x200的整數倍。那麼中間如果有空閒的地方有啥用,其實沒啥用,哪些空白你想怎麼改怎麼改;第一個節開始的地方就是緊接著頭大小後面的部分;同樣這種檔案對齊也適用於節,當大小不滿足的時候在後面填0對齊。

最後,我們來講解本篇的主題

通過上面的講解,我們再來看下面的圖就一目瞭然了:

在這裡插入圖片描述
從圖中,我們可以看出,一個PE檔案,在硬碟中的狀態和在記憶體中狀態是不一樣的,在檔案中要考慮檔案對齊,在記憶體中要考慮記憶體對齊,接下來我們將notepad.exe執行起來,用winhex點選工具,然後選擇在記憶體中開啟,選擇notepad.exe代開,然後我們就可以對比其有什麼不同了,我們先來看看其二進位制,左邊為檔案中的notepad.exe,又邊為記憶體中的,如圖:
在這裡插入圖片描述
在這裡插入圖片描述
我們可以看到除了偏移外,其值都是一樣的,那麼後面也都是一樣的嗎?
不一樣,在檔案中第一個節是從0x400開始的,如圖:
在這裡插入圖片描述
在記憶體中,第一個節從起始位置偏移0x1000處開始:
在這裡插入圖片描述
為什麼沒有從0x400處開始,原因是由記憶體對齊值決定的,在擴充套件頭中我們找一下記憶體對齊欄位SectionAlignment,如下圖:
在這裡插入圖片描述
從這裡我們可以看出它的值是0x1000,現在我們清楚記憶體中為什麼偏移了0x1000了。

附錄

提供一個列印各個結構體大小的C++程式:

#include <windows.h>
#include <winNT.h>
#include <iostream>

using namespace std;

int main()
{
	cout << "IMAGE_DOS_HEADER:    " << sizeof(IMAGE_DOS_HEADER) << endl;
	cout << "IMAGE_NT_HEADERS32:    " << sizeof(IMAGE_NT_HEADERS32) << endl;
	cout << "IMAGE_FILE_HEADER:    " << sizeof(IMAGE_FILE_HEADER) << endl;
	cout << "IMAGE_OPTIONAL_HEADER32:    " << sizeof(IMAGE_OPTIONAL_HEADER32) << endl;
	cout << "IMAGE_SECTION_HEADER:    " << sizeof(IMAGE_SECTION_HEADER) << endl;
	getchar();
	return 0;
}