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;
}