學習成為人體 PE Parser - ianchen0119/About-Security GitHub Wiki
PE (Portable Executable) 是一種用於可執行文件、目標文件和動態連結庫的文件格式,主要使用在 32 位和 64 位的 Windows 作業系統上。
有點像是 Linux 作業系統中的 elf 檔。
在一串連續的記憶體中, DOS Header 一定會是記憶體中的首段內容, DOS Header 中的幾項資訊會是比較重要的:
-
e_magic
e_magic 幫助我們辨認該 PE 檔案是否合法,一般來說,它應該永遠等於 MZ 字串。 如果以 C/C++ 檢查 PE File ,可以這樣做:
#include <windows.h> // ... void parser(char* filePtr){ IMAGE_DOS_HEADER* dosHdr = (IMAGE_DOS_HEADER *)filePtr; if(dosHdr->e_magic != IMAGE_DOS_SIGNATURE){ return; } // ... } // ...
-
e_lfanew
觀察 e_lfanew 之前,必須先了解什麼是 RVA (Relative Virtual Address), RVA 是程式入口點的參考位址,舉例來說: 如果程式被放入虛擬地址(Virtual address, VA)的
0x01000000
處,且 RVA 位於0x102D6C
處,那麼程式在記憶體中的實際入口就會是 VA + RVA:0x01000000 + 0x00102D6C = 0x01102D6C
e_lfanew 其實就是指向了 NT Headers 的 RVA ,換個角度思考,我們將前面的 dosHdr 加上偏移量 (在這邊指 RVA),就可以獲得 NT Headers 的起始位址:
IMAGE_NT_HEADERS* ntHdrs = (IMAGE_NT_HEADERS *)((size_t)dosHdr + dosHdr->e_lfanew);
透過讀取 DOS Header 獲得 NT Headers 的起始位址以後,我們就可以對 PE 檔案做更進一步的檢驗。 NT Headers 共包含了兩大結構,分別是 File Header 以及 Optional Header 。
參考上圖,在 File Header 結構中有多個屬性,每個屬性代表的資訊如下:
-
Machine 紀錄 PE 檔案所存放的機械碼屬於哪一種指令集架構:
- x86
- ARM
- x64
-
NumberOfSections 一個 PE File 通常會有好幾段塊狀區域, NumberofSections 紀錄了 PE 檔案的區段數量。
這個參數對我們撰寫程式解析 PE File 非常有幫助,至於那些塊狀區段存了什麼,晚點會提到。
-
TimeDateStamp 紀錄程式編譯時間的時間戳。
-
PointerToSymbolTable 符號表地址,用於除錯,一般為 0 。
-
NumberOfSymbols 如果符號表存在,這邊會記錄符號數量。
-
Characteristics 紀錄了整個 PE 的屬性,包含:
- Executable
- Info of redirection
- 32-bit or not
- DLL modules
Optional Header 的中文稱可選段,實際上,如果要讓 PE 能夠順利地被執行程式裝載器使用, Optional Header 為必備的。
補充: Optional Header 不存在於 Object File (COFF),而是在編譯的連結階段才會由連結器補上。
參考上圖, Optional Header 包含了很多參數,下面針對重要的參數作介紹:
-
Address of entry point
程式碼編譯後,程式的入口點,也就代表當 Program 被作業系統載入時, Process 會從這邊開始執行。
一般來說,入口點會指向 .text section 的函式開頭。
-
ImageBase
記錄了 PE 檔案 mapping 到記憶體上的預設位址,通常為
0x400000
或是0x800000
。 -
SizeOfImage
記錄了當程式處於動態執行階段需要多少空間才能存放整個 Image 。
-
Section alignment
動態的區域對齊, 32-bit 的環境下預設大小為 0x1000 bytes 。
-
File alignment
靜態的區域對齊, 32-bit 的環境下預設大小為 0x200 bytes 。
假設有不足 0x200 bytes 的資料要放進塊狀區段,塊狀區段的大小為 0x200 bytes ,如果資料多於預設大小,塊狀區段的大小則為 0x400 bytes 。
-
Size of headers
DOS Header + NT Headers + Section Headers 的大小。
-
Data directory
- Export table
- Import table
- Ressource table
- Exception table
- Import Address table
Section Headers 的位址緊隨在 NT Headers 的後方,使用 C/C++ 可以輕鬆的獲得其位址:
IMAGE_SECTION_HEADER* sectHdr = (IMAGE_SECTION_HEADER *)((size_t)ntHdrs + sizeof(*ntHdrs));
至於 Section Headers 的本體到底是什麼呢?它其實就是一個存放塊狀區段資訊的陣列:
for (size_t i = 0; i < ntHdrs->FileHeader.NumberOfSections; i++){
printf("\t#%.2x - %8s - %.8x - %.8x \n", i, sectHdr[i].Name, sectHdr[i].PointerToRawData, sectHdr[i].SizeOfRawData);
}
每一塊存放塊狀區段資訊的空間都會有以下屬性:
-
PointerToRawData
該區段處存在靜態檔案的偏移量。
-
SizeofRawData
該區段的實際大小。
-
VirtualAddress
相較於映像基址的相對偏移量。
-
VirtualSize
顯示該區段需要被分配多少動態空間。
-
Characteristics
紀錄該區段是否可讀、可寫、可執行。
-
.text
用於存放程式碼。
-
.data
用來宣告已初始化的資料與常數。
-
.bss
存放已宣告但尚未初始化的變數。
-
.rdata
存放唯讀資料。
-
.idata
存放引入的函式與資料,這些資料會在 Process 建立時,由執行程式裝載器負責填充。
-
.edata
存放用來導出給其他程式使用的函式與資料。
-
.rsrc
用於記錄程式使用了哪些資源。
-
reloc
重定位,當 PE 程式載入失敗,會以此段作為參考進行調整。
了解 PE File 的基本結構後,我們就可以將惡意 shellcode 添加至目標檔案再將 Entry point 指向惡意程式區段的 Virtual Address 做些壞壞的事(?)
本文章介紹的內容大概只有 Windows APT Warfare:惡意程式前線戰術指南整本書的皮毛,如果想學更多就去下單買一本 Windows APT Warfare:惡意程式前線戰術指南吧!