深入剖析PE檔案

saucej發表於2014-09-25

一、 基本結構。

上圖便是PE檔案的基本結構。(注意:DOS MZ Header和部分PE header的大小是不變的;DOS stub部分的大小是可變的。)
一個PE檔案至少需要兩個Section,一個是存放程式碼,一個存放資料。NT上的PE檔案基本上有9個預定義的Section。分別是:.text, .bss, .rdata, .data, .rsrc, .edata, .idata, .pdata, 和 .debug。一些PE檔案中只需要其中的一部分Section.以下是通常的分類:
l 執行程式碼Section , 通常命名為: .text (MS) or CODE (Borland)
l 資料Section, 通常命名為:.data, .rdata, 或 .bss(MS) 或 DATA(Borland).
l 資源Section, 通常命名為:.edata,
l 輸入資料Section, 通常命名為:.idata
l 除錯資訊Section,通常命名為:.debug
這些只是命名方式,便於識別。通常與系統並無直接關係。通常,一個PE檔案在磁碟上的映像跟記憶體中的基本一致。但並不是完全的拷貝。Windows載入器會決定載入哪些部分,哪些部分不需要載入。而且由於磁碟對齊與記憶體對齊的不一致,載入到記憶體的PE檔案與磁碟上的PE檔案各個部分的分佈都會有差異。

當一個PE檔案被載入到記憶體後,便是我們常說的模組(Module),其起始地址就是所謂的HModule.
二、 DOS頭結構。
所有的PE檔案都是以一個64位元組的DOS頭開始。這個DOS頭只是為了相容早期的DOS作業系統。這裡不做詳細講解。只需要瞭解一下其中幾個有用的資料。

1. e_magic:DOS頭的標識,為4Dh和5Ah。分別為字母MZ。
2. e_lfanew:一個雙字資料,為PE頭的離檔案頭部的偏移量。Windows載入器通過它可以跳過DOS Stub部分直接找到PE頭。
3. DOS頭後跟一個DOS Stub資料,是連結器連結執行檔案的時候加入的部分資料,一般是“This program must be run under Microsoft Windows”。這個可以通過修改連結器的設定來修改成自己定義的資料。
三、 PE頭結構。
PE頭的資料結構被定義為IMAGE_NT_HEADERS。包含三部分:(DW檔案頭和可選映象頭)

1. Signature:PE頭的標識。雙字結構。為50h, 45h, 00h, 00h. 即“PE”。

2. FileHeader:20位元組的資料。包含了檔案的物理層資訊及檔案屬性。

這裡主要注意三項。

l NumberOfSections:定義PE檔案Section的個數。如果對PE檔案新增或刪除Section的話,一定要記的修改此域。

l SizeOfOptionalHeader:定義OptionHeader結構的大小。

l Characteristics:主要用來標識當前的PE檔案是執行檔案還是DLL。其各位都有具體的含義。

資料位
Windows.inc的預定義
為1時的含義
0
IMAGE_FILE_RELOCS_STRIPPED
檔案中不存在重定位資訊
1
IMAGE_FILE_EXECUTABLE_IMAGE
檔案是可執行的
2
IMAGE_FILE_LINE_NUMS_STRIPPED
不存在行資訊
3
IMAGE_FILE_LOCAL_SYMS_STRIPPED
不存在符號資訊
7
IMAGE_FILE_BYTES_REVERSED_LO
小尾方式
8
IMAGE_FILE_32BIT_MACHINE
只在32位平臺執行
9
IMAGE_FILE_DEBUG_STRIPPED
不包含除錯資訊
10
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP
不能從可移動盤執行
11
IMAGE_FILE_NET_RUN_FROM_SWAP
不能從網路執行
12
IMAGE_FILE_SYSTEM
系統檔案。不能直接執行
13
IMAGE_FILE_DLL
DLL檔案
14
IMAGE_FILE_UP_SYSTEM_ONLY
檔案不能在多處理器上執行
15
IMAGE_FILE_BYTES_REVERSED_HI
大尾方式

3. OptionalHeader:總共224個位元組。最後128個位元組為資料目錄(Data Directory)。

以下是欄位的說明:
l AddressOfEntryPoint:程式入口點地址。但載入器要執行載入的PE檔案時要執行的第一個指令的地址。它是一個RVA(相對虛擬地址)地址。一些對PE檔案插入程式碼的程式就是修改此處的地址為要執行的程式碼,然後再跳轉回此處原來的地址。
l ImageBase:PE檔案被載入到記憶體的期望的基地址。對於EXE檔案,通常載入後的地址就期望的地址。但是DLL卻可能是其他的。因為如果這個地址被佔,系統就會重新分配一塊新的記憶體,同時會修改此處載入後的地址。EXE檔案通常是400000h.
l SectionAlignment:每一個Section的記憶體對齊粒度。比如:此值為4096(1000h),那麼每一個Section的起始地址都應該是4096(1000h)的整數倍。如果第一個Section的地址是401000h,大小為100個位元組。那麼下一個Section的起始地址為402000h.。兩個Section之間的空間大部分是空的,未用的。
l FileAlignment:每一個Section的磁碟對齊粒度。比如,此值為512(200h),那麼每一個Section在檔案內的偏移位置都是512(200h)的整數倍。與SectionAlignment同理。
l SizeOfImage:PE檔案在記憶體空間整個映像的大小。包含所有的頭及按SectinAlignment對齊的所有的Section。
l SizeOfHeaders:所有的頭加上Section表的大小。也就是檔案大小減去檔案中所有Section的大小。可以用這個值獲取PE檔案中第一Section的位置。
l DataDiretory:16個IMAGE_DATA_DIRECTORY結構的陣列。每一個成員都對應一個重要的資料結構,比如輸入表,輸出表等。

有兩個地方需要注意:
l 如果PE header裡的最後兩個欄位被賦予一個偽造的值的話,比如:
n LoaderFlags = ABDBFFFFh (其預設值為0)
n NumberOfRvaAndSizes = DFFDEEEEh (其預設值為10h)
一些除錯工具或反編譯工具會認為這個PE檔案是損壞的。有的會直接執行,如果是病毒的話,就會被直接感染;有的則會重啟工具。所以最好在檢視除錯一個PE檔案前,先看一下這裡的取值是否被人賦予一個偽造的很大的值。如果是的話,先修改成預設的值。
l 有人可能注意到在一些PE檔案(MS的連結器連結的PE檔案)的DOS Stub部分跟PE header部分之間存在一部分垃圾資料。標識為其倒數第二非0的雙位元組是一個“Rich ”。這部分資料包含了一些加密資料,來標識編譯這個PE檔案的元件。可用來檢舉某些病毒程式所編譯的程式來自哪臺機器。

四、 資料目錄結構(Data Directory)。
DataDirectory是OptionalHeader的最後128個位元組,也是IMAGE_NT_HEADERS的最後一部分資料。它由16個IMAGE_DATA_DIRECTORY結構組成的陣列構成。IMAGE_DATA_DIRECTORY的結構如下:

每一個IMAGE_DATA_DIRECTORY都是對應一個PE檔案重要的資料結構。他們分別如下:

VirtualAddress指的是對應資料結構的RVA地址;iSize指的是對應資料結構的大小(位元組單位)。一個PE檔案一般只包含其中的一部分,也就是其中一部分資料結構是有資料的;另一部分則都是0。比如,EXE檔案一般都存在IMAGE_DIRECTORY_ENTRY_IMPORT(輸入表),而不存在IMAGE_DIRECTORY_ENTRY_EXPORT(輸出表)。而DLL則兩者都包含。下圖就是某一個PE檔案的資料目錄:

 

五、 Section表。
Section表緊跟在PE header後面。由IMAGE_SECTION_HEADER資料結構組成的陣列。每一個包含了對應Section在PE檔案中的屬性和偏移位置。

 

 

這裡不是所有的成員都是有用的。
l Name1: 塊名,這是一個8位ASCII碼名,用來定義塊名。多數塊名以一個"."開始(如.text),儘管許多PE文件都認為這個"."實際上並不是必須的。值得注意的是,如果塊名超過8位,則最後的NULL不存在。帶有一個"$"的區塊名字會從連結器那裡得到特殊的對待,前面帶"$"的相同名字的區塊被合併,在合併後的區塊中它們是按"$"後面的字元字母順序進行合併的。
l Misc.VirtualSize : 指出實際的、被使用的區塊大小。如果VirtualSize大於SizeOfRawData,那麼SizeOfRawData來自於可執行檔案初始化資料的大小,與VirtualSize相差的位元組用0填充。這個欄位在OBJ檔案中設為0。
l VirtualAddress : 該塊裝載到記憶體中的RVA。這個地址是按照記憶體頁對齊的,它的數值總是SectionAlignment的整數倍。在MS工具中,第一塊的預設RVA為1000H.在OBJ中,該欄位沒意義。如果該值為1000H, PE檔案被載入到400000H,那麼該Section的起始地址為401000H。
l SizeOfRawData : 該塊在磁碟檔案中所佔的大小。在可執行檔案中,這個值必須是PE頭部指定的檔案對齊大小的倍數。如果是0,則說明區塊中的資料是未初始化的。該塊在磁碟檔案中所佔的大小,這個數值等於VirtualSize欄位的值按照FileAlignment的值對齊以後的大小。例如,FileAlignment的大小為1000H,如果VirtualSize中的塊長度為2911,則SizeOfRawData為3000H}
l PointerToRawData : 該塊在磁碟檔案中的偏移。對於可執行檔案,這個值必須是PE頭部指定的檔案對齊大小的倍數。
l PointerToRelocations : 這部分在EXE檔案中無意義。在OBJ檔案中,表示本塊重定位資訊的偏移量。在OBJ檔案中如果不是零,則會指向一個IMAGE_RELOCATION的資料結構。
l NumberOfRelocations : 由PointerToRelocations指向的重定位的數目。
l NumberOfLinenumbers : 由NumberOfRelocations指向的行號的數目,只在COFF樣式的行號被指定時使用。
l Characteristics : 塊屬性,該欄位是一組指出塊屬性(如程式碼/資料/可讀/可寫等)的標誌。多個標誌值通過OR操作形成Characteristics的值。這些標誌很多都可以通過連結器/SECTION選項設定。

PE檔案工具有關區段的屬性地址和偏移就是通過讀該表實現的。

資料位在Windows.inc中的預定義
為1時的含義

IMAGE_SCN_CNT_CODE (00000020H)
節中包含程式碼

IMAGE_SCN_CNT_INITIALIZED_DATA (00000040H)
節中包含已初始化資料

IMAGE_SCN_CNT_UNINITIALIZED_DATA (00000080H)
節中包含未初始化資料
25
IMAGE_SCN_MEM_DISCARDABLE (02000000H)
節中的資料在程式開始後將被丟棄
26
IMAGE_SCN_MEM_NOT_CACHED (04000000H)
節中的資料不會經過快取
27
IMAGE_SCN_MEM_NOT_PAGED (08000000H)
節中的資料不會被交換到磁碟
28
IMAGE_SCN_MEM_SHARED (10000000H)
節中的資料將被不同的程式所共享
29
IMAGE_SCN_MEM_EXECUTE (20000000H)
對映到記憶體後的頁面包含可執行屬性
30
IMAGE_SCN_MEM_READ (40000000H)
對映到記憶體後的頁面包含可讀屬性
31
IMAGE_SCN_MEM_WRITE (80000000H)
對映到記憶體後的頁面包含可寫屬性

六、 PE檔案各個Section。

PE檔案的Sections部分包含了檔案的內容。包括程式碼,資料,資源和其他可執行資訊。每一個Section由一個頭部和一個資料部分組成。所有的頭部都存放在緊跟PE header後的Section表內。
1. 執行程式碼。
在NT Windows系統內,所有的PE檔案的程式碼段都存放在一個Section內,通常命名為.text(MS)或CODE(Borland)。這一段包含了早先提起的AddressOfEntryPoint多指地址的指令及輸入表中的jump thunk table。
2. 資料。
l .bss段存放未初始化的資料,包括函式內或源模組內宣告的靜態變數。
l .rdata段存放只讀資料,比如常字串,常量,除錯指示資訊。
l .data 段存放其他所有的資料(除了自動化變數,其存放在棧中)。比如程式的全域性變數。
3. 資源。
.rsrc段包含了一個模組的資源資訊。以資源樹的結構存放資料。需要用工具來檢視。
4. 輸出資料。
.edata段包含了PE檔案的輸出目錄(Export Directory)。
5. 輸入資料。
.idata包含了PE檔案的輸入目錄和輸入地址表。
6. 除錯資訊。
除錯資訊存放在.debug段。PE檔案也支援單獨的除錯檔案。Debug段包含除錯資訊,但是除錯目錄卻存放在.rdata內。

7. 執行緒區域性儲存。(TLS)
Windows支援每一個程式包含多個執行緒。每一個執行緒有其私有的儲存空間(TLS)去存放執行緒自身的資料。連結器都會為程式建立一個.tls段來存放TLS模板。當程式建立一個執行緒時,系統就會按照這個模板建立一個執行緒私有的區域性儲存空間。
8. 基重定位。
當載入器載入PE檔案到記憶體的時候,有時候不一定是其預期的基地址。那麼就需要調整內部指令的相對地址。所有需要調整的地址都存放在.reloc段內。

七、輸出Section.

這個Section跟DLL關係比較密切。DLL一般定義兩種函式,內部使用的,和輸出到外部給其他呼叫程式使用的。輸出到外部的函式就儲存在這個Section內。
DLL輸出函式分兩種方式,通過名稱和通過序號輸出。當其他程式需要呼叫DLL的時候,呼叫GetProcAddress,通過設定需要呼叫的函式名稱或函式序號可以呼叫DLL內部輸出的函式。

那麼GetProcAddress是怎麼獲取DLL中真正的輸出函式地址呢?以下是詳細的解說。
PE頭的資料目錄(DATA DIRECTORY)陣列的第一個成員對應的(通過其中的RVA地址可獲得)資料結構是IMAGE_EXPORT_DIRECTORY(這裡稱為輸出目錄)。

 

成員
大小
描述
Characteristics
DWORD
未定義,總是0
TimeDateStamp
DWORD
輸出表的建立時間。與IMAGE_NT_HEADER.FileHeader.TimeDateStamp有相同的定義
MajorVersion
WORD
輸出表的主版本號。未使用,為0
MinorVersion
DWORD
輸出表的次版本號。未使用,為0
nName
DWORD
指向一個ASCII字串的RVA,這個字串是與這些輸出函式關聯的DLL的名稱(比如,Kernel32.dll)。這個值必須定義,因為如果DLL檔案的名稱如果被修改,載入器將使用這裡的名稱。
nBase
DWORD
這個欄位包含用於這個可執行檔案輸出表的起始序數值(基數)。正常情況下為1,但不是一定是。當通過序數來查詢一個輸出函式時,這個值會被從序數裡減去。(比如,如果nBase = 1,被查詢的函式的序數是3,那麼這個函式在序號表的索引是3 -1 = 2)。
NumberOfFunctions
DWORD
輸出地址表(EAT)的條目數。其中一些條目可能是0,意味著這個序數值沒有程式碼和資料輸出。
NumberOfNames
DWORD
輸出名稱表(ENT)的條目數。這個值總是大於或等於NumberOfFunctions。小於的情況發生在符號只通過序數來輸出時。另外,當被賦值的序數裡有數字間隔時也會有小於的情況。這個值也是輸出序數表的長度。
AddressOfFunctions
DWORD
輸出地址表(EAT)的RVA。輸出地址表本身是一個RVA陣列,陣列中的每一個非零的RVA都對應一個被輸出的符號。
AddressOfNames
DWORD
輸出名稱表(ENT)的RVA。輸出名稱表本身是一個RVA陣列。陣列中的每一個非零的RVA都向一個ASCII字串。每一個字串都對應一個通過名稱輸出的符號。這個表是排序。這允許加栽器在查詢一個被輸出的符號時可用二進位制查詢方式。名稱的排序是二進位制的,而不是按字母。
AddressOfNameOrdinals
DWORD
輸出序數表(EOT)的RVA。這個表將ENT中的陣列索引對映到相應的輸出地址條目。
實際上,IMAGE_EXPORT_DIRECTORY結構指向三個陣列和一個ASCII字串表。其中重要的是輸出地址表(EAT,即AddressOfFunctions指向的表), 輸出函式地址指標(RVA)構成了這個表。而ENT和EOT則是可以一起合作來獲取EAT裡對應的地址資料。下圖演示了這個過程。

 

這個被載入的DLL名稱是FOO.DLL 。總共輸出了四個函式,其RVA地址分別為400042,0X400156,0X401256.一個外部呼叫程式需要呼叫BAR的函式,那麼先在輸出名稱表(ENT)裡面查詢名稱為BAR的函式,找到後,根據其在輸出需要表(ENT)中對應的索引號。獲取區中的數值為EAT中的索引值,這裡是4.然後從EAT中根據索引4獲取真正的RVA地址0X400520。以下是注意幾點:

l 輸出序號表(EOT)的存在就是為了是EAT跟ENT之間產生關聯。每一個ENT內的成員(函式名)有且只有一個EAT內的成員(函式地址)對應。但是一個EAT內的成員並不是只有一個ENT內的成員對應。比如,有的函式存在別名的話,就會出現多個ENT內的成員都對應一個EAT內的成員。
l 如果已經獲得一個函式的序號值,那麼就可以直接到EAT內獲得其RVA地址,而不需要經過ENT和EOT進行查詢。但是這樣的按序號輸出的DLL不易於維護。
l 通常情況下,EAT的個數(NumberOfFunctions)必須小於或等於ENT的個數(NumberOfNames)。只有在一個函式按序號輸出時(其在ENT和EOT表裡沒有對應的資料),ENT的數量才有可能少於EAT的數量。比如,總共有70個函式輸出,但是在ENT表裡只有40個,這就意味著剩餘的30個函式是靠序號輸出的。那麼我們如何知道哪些是直接靠序號輸出的呢?只有通過排除法來獲得。把存在在EOT表裡的序號從EAT裡排除出去,剩下的就是靠序號輸出的函式。
l 當通過一個序號值來獲取EAT內的函式RVA時,需要把這個序號值減去nBase的值來獲取在EAT表裡真正的索引位置。而通過名稱查詢則不需要這麼做。
l 輸出轉向。某些時候,你從一個DLL中呼叫的一個函式可能位於另一個DLL中。這就叫輸出轉向。比如,Kernel32.dll中的HeapAlloc就是轉到呼叫NTDLL.dll中的RtlAllocHeap。這種轉向是在連結的時候,在.DEF檔案中定義一個特殊的指令來實現的。那麼當一個函式被轉向後,在其所在EAT表裡對應的資料便不是其地址,而是一個指向表明被轉向的DLL和函式的ASCII字串的地址指標。

上圖就是Kernel32.dll的輸出函式表,其中HeapAlloc的RVA值0x00009048就是一個指向“NTDLL.RtlAllocHeap”的指標。
八 、 輸入Section.
輸入Section通常位於.idata段內。它包含了所有程式需要用到的來自其他DLL的函式的資訊。Windows載入器負責載入所有程式用到的DLL到程式空間。然後為程式找到所有其需要用到的函式的地址。下面描述這個過程:
PE頭的資料目錄(DATA DIRECTORY)陣列的第二個成員對應的(通過其中的RVA地址可獲得)資料結構是輸入表。輸入表是一個 IMAGE_IMPORT_DESCRIPTOR資料結構的陣列。沒有欄位表明這個陣列的個數,只是它的最後一個成員的資料都為0。每一個陣列成員都對應 一個DLL。

成員
大小
描述
OriginalFirstThunk
DWORD
指向輸入名稱表(INT)的RVA。INT是由IMAGE_THUNK_DATA資料結構構成的陣列。陣列中的每一個成員定義了一個輸入函式的資訊,陣列最後以一個內容為0的IMAGE_THUNK_DATA結束。
TimeDateStamp
DWORD
當執行檔案不與被輸入的DLL進行繫結時,這個欄位為0。當以舊的方式繫結時,這個欄位包括時間/日期。當以新的樣式繫結時,這個欄位為-1。
ForwarderChain
DWORD
這是第一個被轉向的API的索引。老樣式繫結的定義。
Name
DWORD
指向被輸入DLL的ASCII字串的RVA。
FirstThunk
DWORD
指向輸入地址表(IAT)的RVA。IAT也是一個IMAGE_THUNK_DATA資料結構的陣列。
由上表可知,輸入表主要是通過IMAGE_THUNK_DATA這個資料結構匯入函式。下面是IMAGE_THUNK_DA他的描述

這是一個DWORD聯合體資料結構。其實這裡對輸入表有意義的欄位只有兩個,Ordinal和 AddressOfData。當這個DWORD資料的最高位為1的時候,代表函式以序號的方式匯入,Ordinal的低31位就是輸入函式在其DLL內的 匯出序號。當這個DWORD的資料最高位為0的時候,代表函式以字串方式匯入。AddressOfData就是一個指向用來匯入函式名稱的 IMAGE_IMPORT_BY_NAME的資料結構的RVA。(這裡用來判斷最高位的值0x8000000,預定義值為 IMAGE_ORDINAL_FLAG32。)

 

 

l Hint欄位也表示函式的序號,主要是用來便與載入器快速查詢在匯入的DLL的函式匯出表,當通過這個序號查詢到的函式跟所要匯入的函式不匹配時,就改為通過名稱查詢。不過這個欄位是可選的,有些編譯器把它設定為0。

l Name1欄位定義了匯入函式的名稱字串,這是一個以0為結尾的字串。

整個過程有點複雜,下圖給出一個相對清晰的描述。

 

1:載入器首先讀入IMAGE_IMPORT_DESCRIPTOR,獲得需要載入的動態庫User32.Dll。

2:載入器根據OriginalFirstThunk或FirstThunk所指向的IMAGE_THUNK_DATA陣列的RVA來獲取真正的輸入函式名稱表INT和輸出函式地址表IAT。這裡這兩個表所指向的是同一個IMAGE_IMPORT_BY_NAME資料結構的RVA.

3:載入器根據IMAGE_IMPORT_BY_NAME的需要或名稱到匯入的DLL(User32.dll)函式匯出表中獲取輸入函式的地址。然後把這個地址替換掉FristThunk所指向的函式輸入地址表中的資料。

上圖已經說明了為什麼會存在兩個一摸一樣的IMAGE_THUNK_DATA陣列。答案就是這個PE檔案被裝入記憶體後。FirstThunk所指向的IMAGE_THUNK_DATA內的值將被改為用來儲存匯入函式的整整地址。稱之為IAT.其實在資料目錄表

DATA_DIRECTORY中的第13項(索引值為12)直接給出了這個IAT的地址和大小,可以直接通過資料目錄快速獲得這個IAT表。但是這樣還不足以說明為什麼會存在兩個一樣的IMAGE_THUNK_DATA陣列。INT好像沒有存在的必要,這裡需要設計到一個

繫結的概念。

繫結:在載入器載入PE檔案時候,先要堅持輸入表獲取要輸入的DLL名稱,然後把DLL對映到程式的地址空間。再檢查IAT表的IMAGE_THUNK_DATA陣列所指向的字串獲取要輸入函式的名稱,然後用輸入函式的地址替換掉IMAGE_THUNK_DATA陣列內

的資料。整個過程需要相對比較長的時間。如果事先在連結的時候就把這個地址寫入IAT中,那麼會節省很多時間。這就是繫結的由來。

再繫結後,PE檔案IAT表裡存放著的是匯入DLL輸出函式的實際記憶體地址。要使繫結的結果能正常執行,需要兩個條件:

     1在載入PE檔案所需的DLL時候,DLL應該被對映到他們自己PE頭裡定義好的ImageBase這個地址。

      2被執行繫結後,PE檔案所匯入DLL的函式匯出的函式表裡的函式符號的位置不能發生改變。

這 兩個條件當然很難在長時間內很難滿足。比如,這個被匯入的DLL發生了變化,增加了新的函式輸出。那麼其原來輸出表內的函式符號的位置發生了變化。那麼這 個時候,原先繫結的結果就會發生錯誤。為了解決這個問題,所以就同時定義了INT這個表。讓它做為IAT的備份。一旦預先繫結好的IAT發生了錯誤,那麼 載入器便會從INT裡獲取所需要的資訊。

這就是為什麼會存在兩個一模一樣的IMAGE_THUNK_DATA陣列真正的緣由。微軟的連結器一般總會在生成IAT的同時生成一個INT;而Borland的連結器卻只生成IAT。所以Borland生成的PE檔案是不能被繫結的。
那麼,當載入器載入PE檔案的時候,需要判斷當前的繫結是否有效。在資料目錄(Data Directory)的第12項(序號為11)所指向的一組資料結構IMAGE_BOUND_IMPORT_DESCRIPTOR就是用來檢查這個有效性的。

成員
大小
描述
TimeDateStamp   DWORD               必須與被輸入的DLL的PE頭內的TimeDateStamp一樣,如果不一致,那麼載入器就會認為繫結的物件有誤,需要重新修補輸入表。
OffsetModuleName  WORD                    第一個IMAGE_BOUND_IMPORT_DESCRIPTOR結構到被輸入DLL名稱的偏移(非RVA)。
NumberOfModuleForwarderRefs  WORD.  包含緊跟在這個結構後面IMAGE_BOUND_FORWARDER_REF的數目。

這個結構跟IMAGE_BOUND_IMPORT_DESCRIPTOR其實很象除了最後一個成員。它主要用於,在被匯入的DLL中的某一個函式是轉向匯出時,這個結構就用來給出所轉向到的函式的資訊。

延遲載入:
除了通過載入器建立IAT表以外,程式呼叫外部DLL函式還有另外一種方式。就是先通過LoadLibrary動態載入DLL,然後用GetProcAddress獲取所需函式的地址。這種方式稱之為“延遲載入”。
資料目錄(Data Directory)第14個成員(序號是13)IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT條目就是指向延遲載入的資料。這個資料就是由一個名叫ImgDelayDescr資料結構組成的陣列。
ImgDelayDescr = packed record
grAttrs: DWORD;
szName: DWORD;
phmod: PDWORD;
pIAT: TImageThunkData32;
pINT: TImageThunkData32;
pBoundIAT: TImageThunkData32;
pUnloadIAT: TImageThunkData32;
dwTimeStamp: DWORD;
end;
成員
描述
grAttrs
設為1的時候,下面的各個成員都是RVA,否則是VA(虛擬地址)。
szName
指向一個DLL名稱的RVA。
phmod
指向一個HMODULE的RVA。
pIAT
指向DLL的IAT的RVA。
pINT
指向DLL的INT的RVA。
pBoundIAT
可選的繫結IAT的RVA。
pUnloadIAT
指向DLL的IAT的未繫結拷貝
dwTimeStamp
延遲裝載的輸入DLL的時間/日期。通常是0。

九、 Windows載入器

載入器賭氣一個PE檔案的過程如下:

1:先讀入PE檔案的DOS頭,PE(包含IMAGE_FILE_HEADER檔案頭和IMAGE_OPTIONAL_HEADER32可選映象頭),和section頭。

2根據PE頭的ImageBase所定義的載入地址是否可用,如果已被其他模組佔用,重新分配一塊空間

3:根據section頭部的資訊,把檔案的各個seciton對映到分配的空間,並根據各個seciton定義的資料來修改所對映的頁屬性。

4:如果檔案被載入的地址不是imageBase定義地址,重新修正ImageBase。

5:根據PE檔案的輸入表載入所需要的DLL到空間。

6:然後替換IAT表內資料位實際呼叫函式地址。

7:根據PE頭內的資料生成初始化的堆和棧。

8:建立初始化執行緒,開始執行程式。

這裡要提的是載入PE檔案所需要的DLL的過程是建立在六個底層API上的 

LdrCheckForLoadedDll:

LdrMapDll:

LdrWalkImportDescriptor://遍歷模組的輸入表來載入其所需的其他模組。

LdrUpdateLoadCount:計數器模組的使用次數。

LdrRunInitializeRoutines:初始化模組。

LdrClearLoadInProgress:清楚某些標誌,表明載入已經成功。

十、 插入程式碼到PE檔案

有三種方式可以插入程式碼到PE檔案:

1. 把程式碼加入到一個存在的Section的未用空間裡。

2. 擴大一個存在的Section,然後把程式碼加入。

3. 新增一個Section。

方法一、增加程式碼到一個存在的Section。
首先我們需要找到一個被對映到一個塊有執行許可權的Section。最簡單的方式就是直接利用CODE Section。
然後我們需要查詢這塊Section內的多餘空間(也就是填滿了00h)。我們知道一個Section有兩個資料來表示其大小。 VirtualSize和SizeOfRawData。這個VirtualSize代表Section裡程式碼實際所佔用的磁碟空間。 SizeOfRawData代表根據磁碟對齊後所佔的空間。通常SizeofRawData都會比VirtualSize要大。如下圖。

圖中的SizeOfRawData是0002A000,而VirtualSize是00029E88。當PE檔案被載入到記憶體的時候,他們之間 的多餘空間的資料是不會被載入到記憶體去。那麼如果要把加入到這個間隙中間的程式碼也被載入到記憶體去,就需要修改VirtualSize的值,這裡把 VirtualSize的值可以改為00029FFF。這樣,我們就有了一小段空間加入自己的程式碼。下面需要做的就是先找到PE檔案的入口點 OriginalEntryPoint,比如這個OriginalEntryPoint是0002ADB4,ImageBase是400000,那麼入口 點的實際虛擬地址是0042ADB4。然後計算出自己程式碼的起始RVA,更換掉PE頭內的OriginalEntryPoint,在自己的程式碼最後加上:
MOV EAX,00042ADB4
JMP EAX
這樣就可以在PE檔案被載入的時候,先執行自己的程式碼,然後再執行PE檔案本身的程式碼。成功的把程式碼加入到了PE檔案內。
方法二、擴大一個存在的Section來加入程式碼。
如果在一個Section末尾沒有足夠的空間存放自己的程式碼,那麼另外一種方法就是擴大一個存在的Section。一般我們只擴大PE檔案最尾部的Section,因為這樣可以避免很多問題,比如對其他Section的影響。
首先我們的找到最後一個Section使之可讀可執行。這可以通過修改其對應Section頭部的Characteristics來獲得。然後 根據PE頭內檔案對齊的大小,修改其SizeOfRawData。比如檔案對齊的大小是200h,原先SizeOfRawData=00008000h, 那麼我們增加的空間大小應該是200h的整數倍,修改完的SizeOfRawData至少是00008200h。增加完空間後,需要修改PE頭內的兩個字 段的數值,SizeOfCode和SizeOfInitialishedData。分別為它們增加200h的大小。這樣我們就成功的擴大了一個 Section,然後根據方法一內的方式把程式碼加入到增加的空間。
方法三、新增一個Section來加入程式碼。
如果要加入的程式碼很多,那麼就需要新增一個Section來存放自己的程式碼。
l 首先,我們需要在PE頭內找到NumberOfSections,使之加1。
l 然後,在檔案末尾增加一個新的空間,假設為200h,記住起始行到PE檔案首部的偏移。假如這個值是00034500h。同時將PE頭內的SizeOfImage的值加200h。
l 然後,找到PE頭內的Section頭部。通常在Section頭部結束到Section資料部分開始間會有一些空間,找到Section頭部的最後然後加入一個新的頭部。假設最後一個Section頭部的資料是:
1. Virtual offset : 34000h
2. Virtual size : 8E00h
3. Raw offset: 2F400h
4. Raw size : 8E00h
而檔案對齊和Section對齊的資料分別是:
5. Section Alignment : 1000h
6. File Alignment : 200h
l 那麼新增加的Section必須與最後一個Section的邊界對齊。它的資料分別:
1. Virtual offset : 3D000h (因為最後一個Section的最後邊界是34000h + 8E00h = 3CE00h,加上Section對齊,則Virtual offset的值為3D000h)。
2. Virtual size : 200h。
3. Raw offset: 00034500h。
4. Raw size: 200h.
5. Characteristics : E0000060 (可讀、可寫、可執行)。
l 最後,只需要修改一下PE頭內的SizeOfCode和SizeOfInitialishedData兩個欄位,分別加上200h。
l 剩下的就是按照方法一的方式把程式碼放入即可。
十一、 增加執行檔案的輸入表專案。
在一些特殊用途上,我們需要為執行檔案或DLL增加其不包含的API。那麼可以通過增加這些API在輸入表中的註冊來達到。
1. 每一個輸入的DLL都有一個IMAGE_IMPORT_DESCRIPTOR (IID)與之對應。PE頭中的最後一個IID是以全0來表示整個IID陣列的結束。
2. 每一個IID至少需要兩個欄位Name1和FirstThunk。其他欄位都可以設定為0。
3. 每一個FirstThunk的資料必須是一個指向IMAGE_THUNK_DATA陣列的RVA。每一個IMAGE_THUNK_DATA又包含了指向一個API名稱的RVA。
4. 如果IID陣列發生改變,那麼只需要修改資料目錄陣列中對應輸入表的資料結構IMAGE_DATA_DIRECTORY的iSize。
增加一個新的IID到輸入表的末尾,就是把輸入表末尾的全是0的IID修改成增加的新的IID,然後在增加一個全0的IID作為輸入表新的末 尾。但是如果在輸入表末尾沒有空間的話,那就需要拷貝整個輸入表到一個新的足夠的空間,同時修改資料目錄陣列對應輸入表的資料結構 IMAGE_DATA_DIRECTORY的RVA和iSize。
步驟一、增加一個新的IID。
把整個IID陣列移到一個有足夠空間來增加一個新的IID的地方。這個地方可以是.idata段的末尾或是新增一個Section來存放。
修改資料目錄陣列對應輸入表的資料結構IMAGE_DATA_DIRECTORY的RVA和iSize。
如果必要,將存放新IID陣列的Section大小按照Section Alignment向上取整(比如,原來大小是1500h, 而section Alignment為1000h,則調整為2000h)以便於整個段可以被對映到記憶體。
執行移動過IID陣列的執行檔案,如果正常的話,則進行第二步驟。如果不工作的話,需要檢查新增的IID是否已經被對映到記憶體及IID陣列新的偏移位置是否正確。
步驟二、增加一個新的DLL及其需要的函式。
在.idata節內增加兩個以null結尾的字串,一個用來存放新增的DLL的名字。 一個用來存放需要匯入的API的名稱。這個字串前需要增加一個為null的WORD欄位來構成一個 Image_Import_By_Name資料結構。
計算這個新增的DLL名稱字串的RVA.
把這個RVA賦予新增的IID的Name1欄位。
再找到一個DWORD的空間,來存放Image_Import_by_name的RVA。這個RVA就是新增DLL的IAT表。
計算上面DWORD空間的RVA,將其賦予新增IID的FirstThunk欄位。
執行修改完的程式。
====================
PE檔案基礎補註

 

 

相關文章