0x01 前言
PE文件是指windows系統下使用的可執行文件格式,它是微軟在unix平台的COFF基礎上制作而成的。PE文件一般指32位的可執行文件,也稱為PE32。64位的可執行文件稱為PE+或者PE32+(不是PE64),是PE32的一種擴展形式。
0x01 簡介
要了解PE文件,首先要知道PE格式,那么什么是PE格式呢,既然是一個格式,那肯定是需要遵循的一定的定理。其實PE格式就是各種結構體的結合,這些結構體都定義在在WinNT.h
這個頭文件中。
PE文件整體結構
一個PE文件大致可分為以下幾個部分:
- DOS部分
- PE文件頭
- 節區頭(節表)
- 節數據(塊數據)
- 調試信息
從DOS頭到節區頭是PE頭,其下的是PE體。文件中使用偏移(Offset), 內存中使用VA(Virtual Address)來表示位置。
VA指的是進程虛擬內存的絕對地址,RVA(Relative Virtual Address)指從
某個
基准位置(ImageBase)開始的相對地.址。兩者存在以下換算關系:
RVA +ImageBase = VA
PE頭內部的信息大多以RVA的形式存在
0x02 PE頭
0x02.1 DOS頭
DOS頭結構體大小為40個字節,其中只需要熟悉兩個成員變量:e_magic
與e_lfanew
,前者是DOS簽名,固定為MZ
,取自微軟開發人員Mark Zbikowski
首字母。后者則是指示NT頭的偏移位置(IMAGE_NT_HEADERS
),除了這兩個成員,其他成員全部用0填充都不會影響程序正常運行。IMAGE_DOS_HEADER
定義如下
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;
變量e_lfanew
固定偏移位置3C處,從此后到e_lfanew
所指向的偏移位置為DOS Stud
(中文一般翻譯為DOS存根
),在win32中未使用。在16位系統中運行便輸出一個this programe cannot be urn in DOS mode
就退出了。
如下圖框中的內容便是DOS頭:
由圖可知e_lfanew
的值是000000F8
,則00000040
到000000F4
的內容便為DOS Stud
。
0x02.2 PE文件頭
PE文件頭由PE文件頭標志,標准PE頭,擴展PE頭三部分組成
0x02.2.1 IMAGE_NT_HEADERS
上一節我們有講到DOS頭中的e_lfanew
指向IMAGE_NT_HEADERS
,
我們先看一下這個結構體在winnt.h中的定義:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //固定為'004550', 字符:"PE"
IMAGE_FILE_HEADER FileHeader;//標准PE頭 => 20字節
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//擴展PE頭 => 32位下224字節(0xE0) 64位下240字節(0xF0)
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
由上可知IMAGE_NT_HEADERS
定義了3個成員變量,
Signature
固定為字符PE
;FileHeader
指向一個為IMAGE_FILE_HEADER
的結構體;- 在32位下
OptionalHeader
指向一個IMAGE_OPTIONAL_HEADER32
的結構體。在64位下,OptionalHeader
指向一個IMAGE_OPTIONAL_HEADER64
的結構體。
0x02.2.2 IMAGE_FILE_HEADER
我們一個一個分析,先看下IMAGE_FILE_HEADER
的定義:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //可以運行在什么平台上 任意:0 ,Intel 386以及后續:14C x64:8664
WORD NumberOfSections; //節的數量
DWORD TimeDateStamp; //編譯器填寫的時間戳
DWORD PointerToSymbolTable; //調試相關
DWORD NumberOfSymbols; //調試相關
WORD SizeOfOptionalHeader; //標識擴展PE頭大小
WORD Characteristics; //文件屬性 => 16進制轉換為2進制根據哪些位有1,可以查看相關屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_FILE_HEADER
里面大概有4個重要的成員變量需要掌握,如果這些變量的值設置不正確,程序便不能正常運行。
Machine
:每個CPU都有一個唯一的Machine碼,兼容32位Intel x86芯片的machine碼是14c。其它Machine碼可在winnt.h中查看。NumberOfSections
: 指示文件中存在的節區數量, 此值是一定要大於0,且當定義的節區數與實際的節區數不一樣時,將發生運行錯誤。SizeOfOptionalHeader
:標識IMAGE_NT_HEADERS
第三個成員變量OptionalHeader
的大小。Characteristics
:標識文件屬性,是否是dll,是否可執行等信息。
如下圖便是FileHeader
,
根據圖片所示,我們可以得出各值:
Machine=014c;
NumberOfSections =0004;
TimeDateStamp=4ade5203;
PointerToSymbolTable=00000000;
NumberOfSymbols=00000000;
SizeOfOptionalHeader=00e0;
Characteristics=010f;
0x02.2.2 IMAGE_OPTIONAL_HEADER
擴展PE頭在32位和64位系統上大小是不同的,在32位系統上有224個字節,16進制就是0xE0,與上面的SizeOfOptionalHeader也能對上。
IMAGE_OPTIONAL_HEADER
是PE頭結構體最大的一個,先看定義:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; //PE32: 10B PE64: 20B
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //所有含有代碼的區塊的大小 編譯器填入 沒用(可改)
DWORD SizeOfInitializedData; //所有初始化數據區塊的大小 編譯器填入 沒用(可改)
DWORD SizeOfUninitializedData; //所有含未初始化數據區塊的大小 編譯器填入 沒用(可改)
DWORD AddressOfEntryPoint; //程序入口RVA,指出程序最先執行的代碼起始位置,相當重要
DWORD BaseOfCode; //代碼區塊起始RVA
DWORD BaseOfData; //數據區塊起始RVA
//
// NT additional fields.
//
DWORD ImageBase; //內存鏡像基址(程序默認載入基地址)
DWORD SectionAlignment; //內存中對齊大小
DWORD FileAlignment; //文件中對齊大小(提高程序運行效率)
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //內存中整個PE文件的映射的尺寸,可比實際值大,必須是SectionAlignment的整數倍
DWORD SizeOfHeaders; //所有的頭加上節表文件對齊之后的值
DWORD CheckSum; //映像校驗和,一些系統.dll文件有要求,判斷是否被修改
WORD Subsystem;
WORD DllCharacteristics; //文件特性,不是針對DLL文件的,16進制轉換2進制可以根據屬性對應的表格得到相應的屬性
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;
部分成員變量說明:
-
ImageBase
:指出文件被加載到內存應該被優先加載的內存地址,exe,dll文件被裝載到用戶內存的0~7fffffff
中,sys文件被載入內存的80000000~ffffffff
中,一般情況下,exe會被裝載到00400000
,dll文件的ImageBase
值為10000000
-
NumberOfRvaAndSizes
: 用來指定DataDirectory
數組的個數,在winnt.h中IMAGE_NUMBEROF_DIRECTORY_ENTRIES
被明確定義為16,但PE loader一般會通過此值來識別DataDirectory
的大小,也就是說DataDirectory
的長度不一定都是16。 -
DataDirectory
是由IMAGE_DATA_DIRECTORY
結構體數據組成的,數組中的每一項都有定義,詳細如下:
但我們一般只需要關心幾個常見的即可,導出表、導入表、資源表、TLS表。詳細的我們放到后面再講。
整個擴展頭內容如圖所示。
根據圖片例子,我們把常見的成員變量值列舉出來如下:
Magic=010b
AddressOfEntryPoint=0x0005ec27
BaseOfCode=1000
BaseOfData=8b000
ImageBase=0x00400000
SectionAlignment=1000
FileAlignment=1000
SizeOfImage=dd000
SizeOfHeader=1000
NumberOfRvaAndSizes=10
還需要知道的是,程序的真正入口點 = ImageBase + AddressOfEntryPoint。
0x03.節區頭
節區頭由IMAGE_SECTION_HEADER定義,節區頭的結構如下。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //ASCII字符串 可自定義 只截取8個字節
union { //該節在沒有對齊之前的真實尺寸,該值可以不准確
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //內存中的偏移地址
DWORD SizeOfRawData; //節在文件中對齊的尺寸
DWORD PointerToRawData; //節區在文件中的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //節的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
節區頭的數量由IMAGE_NT_HEADERS
結構中的FileHeader.NumberOfSections
字段來指定的。同時也以一個全部為空的IMAGE_SECTION_HEADER
結構體作為結束,節區頭總是被存放在緊接在PE文件頭的地方。
在節區頭中,我們一般只需要了解以下幾個:
Name
:非必須以NULL結束,也未限制只能使用ASCII,可放入任何值VirtualSize
: 內存中節區所占大小VirtualAddress
: 內存中節區起始地址(RVA)SizeOfRawData
: 磁盤文件中節區所占大小PointerToRawData
:磁盤文件中節區起始位置Charateristics
:節區屬性。
實例:
如圖所示,可知有四個節區,外加一個全為0的節區。
0x04.導入表
先看如何定位導入表起始地址,在IMAGE_OPTIONAL_HEADER
頭中的最后一個成員變量,我們有提到其是一個包含16個元素的IMAGE_DATA_DIRECTORY
數組,其中第2個元素就是導入表的起始位置。
先看IMAGE_DATA_DIRECTORY
的定義
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
即我們可以通過IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress
訪問到導入表的起始地址。
再來看一下定義導入表的結構體:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA 指向 INT (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; //RVA指向dll名字,以0結尾
DWORD FirstThunk; // RVA 指向 IAT (PIMAGE_THUNK_DATA結構數組)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
一個程序導入了多少個庫就有多少個IMAGE_IMPORT_DESCRIPTOR
結構體,這些結構體組成一個數組,且結構體數組以一個全為NULL的結構體作為結束。所以被稱為IMPORT Directory Table
.其中比較重要的成員變量如下(以下地址值全為RVA)
OriginalFirstThunk
:指向INT(導入名稱表 、Improt Name Table)的地址,以全NULL結束。Name
:庫名稱字符串的地址FirstThunk
: 指向IAT(導入地址表、Import Address Table)的地址,以全NULL結束。
舉個例子,隨便找個程序,
IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress
的值為9C 5A 0F 00
即RVA=F5A9C, 換算成RAW則為F4A9C(換算方法見附)。
我們轉到F4A9C的地址查看如下:
OriginalFirstThunk - INT (Import Name Table)
第一個成員變量為OriginalFirstThunk
,它是INT的起始地址,換句話說,就是INT是一個包含導入函數信息的結構體指針數組,每個數組的元素都指向一個IMAGE_IMPORT_BY_NAME
的結構體,並以全為NULL的元素結束。根據上圖我們知道第一個元素的值為F6284(RVA)->換成RAW則為F5284。來到F5284這個地址.如下。由圖可知INT數組長度為5。(以就代表着從這個庫文件里面導入了5個函數)
第一個值為F6648->RAW為F5648。來這這個地址。
要看懂這個結構得先看下IMAGE_IMPORT_BY_NAME
的定義
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
前兩個字節是庫中函數的固有編號,后面的則是一個字符數組,以00結束。所以這個導入的函數則是ColorHLSToRGB
.
Name
根據IMAGE_IMPORT_DESCRIPTOR
的結構可知,第四個成員則為Name
,其值為F666A,換成RAW為F566A,我們轉到這個地址看下,可知其導入的是SHLWAPI.dll
另外,我們也可以通過工具驗證確實從SHLWAPI.dll導入了5個函數。
FirstThunk- IAT (Import Address Table)
由IMAGE_IMPORT_DESCRIPTOR
結構體可知其最后一個成員為FirstThunk
。根據上面的圖可知,第一數組元素的FirstThunk
值為C55F4, RAW為:C45F4。我們來到這個地址處:
第一個數組元素值為F6648.如上便是IAT數組區域,對應於SHLWAPI.dll,數組長度也剛好是5。轉為RAW則為F5648。來這個地址,發現他們指向同一個函數。
既然指向同一個地址,為啥需要兩個去索引,這是因為需要區分PE加載前還是加載后。如果是加載前,那個IAT跟INT一樣,都可以找到依賴的函數名稱,如果是加載后。也就是在內存中的話, 那么IAT表保存的就是函數的地址。
PELoader把導入函數輸入至IAT的步驟
- 讀取IID的Name成員,獲取庫名稱字符串(eg:kernel32.dll)
- 裝載相應庫: LoadLibrary("kernel32.dll")
- 讀取IID的OriginalFirstThunk成員,獲取INT地址
- 逐一讀取INT中數組的值,獲取相應IMAGE_IMPORT_BY_NAME地址(RVA)
- 使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name項,獲取相應函數的起始地址:GetProcAddress("GetCurrentThreadld")
- 讀取IID的FirstThunk(IAT)成員,獲得IAT地址
- 將上面獲得的函數地址輸入相應IAT數組值
- 重復以上步驟4~7,知道INT結束(遇到NULL)
0x5導出表
導出表由結構體IMAGE_EXPORT_DIRECTORY
定義。在PE文件中,IMAGE_EXPORT_DIRECTORY
結構體數組的起始位置由IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress
給出。
我們先看下IMAGE_EXPORT_DIRECTORY
的定義:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 指針指向該導出表文件名字符串
DWORD Base; // 導出函數起始序號
DWORD NumberOfFunctions; // 實際導出函數的個數
DWORD NumberOfNames; // 以函數名字導出的函數個數
DWORD AddressOfFunctions; // 指針指向導出函數地址表RVA
DWORD AddressOfNames; // 指針指向導出函數名稱表RVA
DWORD AddressOfNameOrdinals; // 指針指向導出函數序號表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
函數查找過程 - GetProAddress工作原理
- 利用AddressOfNames成員轉到“函數名稱數組”
- “函數名稱數組”中存儲字符串地址。通過比較字符串,查找指定的函數名稱(此時數組的索引稱為name_index)
- 利用AddressOfNameOrdinals成員,轉到orinal數組
- 在orinal數組中通過name_index查找相應的值
- 利用AddressOfFunction成員轉到“函數地址數組”
- 在“函數地址數組”中將剛剛求得的ordinal用作數組索引,獲得指定的函數起始地址。
有了導入表的基礎,則理解導出表就很簡單了,這里就不再舉例了。
0x6 附
1.RVA to RAW
轉換方法如下:
1).查找RVA所在節區
2).使用如下公司計算。
RAW - PointerToRawData = RVA-VirtualAddress
RAW = RVA-VirtualAddress + PointerToRawData
原理:
在內存中或者在文件中,一個地址相對與該地址所在的區段的偏移大小是固定的。換句話說,它們距離這一個節區開頭的距離都是相同的。所以我們可以先算出相對於其所在區段的偏移大小,再加上對應區段的基地址。
舉個例子,如圖,假如我們要算 RVA a7bd0的raw地址是多少?
1、首先我們確定a7bd0在哪個Virtual Address中(因為是rva,所以當成內存地址看)。可知其在.rdata段,
2、rdata段的基地址是8b000,a7bd0相對首地址相差a7bd0-8b000=1cbd0.
3、再加上rdata段在文件中的首地址8b000(Raw Address).1cbd0+8b000=a7bd0
4、所以rva的a7bd0在文件中的偏移是a7bd0。