深入剖析PE文件

PE文件是Win32的原生文件格式.每一個Win32可執行文件都遵循PE文件格式.對PE文件格式的瞭解可以加深你對Win32系統的深入理解.

一、 基本結構。

上圖便是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。包含三部分:

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 SizeOfImagePE文件在內存空間整個映像的大小。包含所有的頭及按SectinAlignment對齊的所有的Section。

l SizeOfHeaders所有的頭加上Section表的大小。也就是文件大小減去文件中所有Section的大小。可以用這個值獲取PE文件中第一Section的位置。

l DataDiretory16個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選項設置。

數據位在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的名稱是F00.DLL。總共輸出了四個函數,其RVA地址分別爲0x400042、0x400156、0x401256和0x400520。一個外部調用程序需要調用其中一個名爲”Bar”的函數,那麼它先在輸出名稱表(ENT)裏查找名稱爲Bar的函數,找到後,根據其在輸出序號表(EOT)中對應的索引號,獲取其中的數值爲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_DATA的描述:

這是一個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)函數導出表中獲取導入函數的地址。然後把這個地址替換掉FirstThunk所指向的函數輸入地址表中的數據。

上圖已經說明了爲什麼會存在兩個一模一樣的IMAGE_THUNK_DATA數組。答案就是在這個PE文件被裝 入內存後,FirstThunk所指向的IMAGE_THUNK_DATA內的值將被改爲用來存儲導入函數的真正的地址。我們稱之爲IAT(Import Address Table). 其實在數據目錄表DATA_DIRECTORY中的第13項(索引爲12)直接給出了這個IAT的地址和大小. 可以直接通過數據目錄快速獲得這個IAT表. 但是這樣還不足於說明爲什麼會存在兩個一樣的IMAGE_THUNK_DATA數組。INT好象沒有存在的 必要。這裏要涉及到一個綁定的概念。

綁定:

l 在 加載器加載PE文件的時候,先需要檢查輸入表獲取要輸入的DLL的名稱,然後把DLL映射到進程的地址空間。再檢查IAT表裏的 IMAGE_THUNK_DATA數組所指向的字符串獲取要輸入函數的名稱,然後用輸入函數的地址替換掉IMAGE_THUNK_DATA數組內的數據。 整個過程需要相對比較長的時間。如果事先在鏈接的時候就把這些地址寫入IAT中,那麼就會節省很多時間。這就是綁定的由來。

l 再綁定後,PE文件IAT表裏放着是導入DLL輸出函數的實際內存地址。要使綁定的結果能正常運行,需要兩個條件:

n 在加載PE文件所需的DLL的時候,DLL應該被映射到它們自己PE頭裏定義好的ImageBase這個地址。

n 被執行綁定後,PE文件所導入DLL的函數導出的函數表裏的函數符號的位置不能發生改變。

l 這 兩個條件當然很難在長時間內很難滿足。比如,這個被導入的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頭和Section頭。

2. 然後根據PE頭裏的ImageBase所定義的加載地址是否可用,如果已被其他模塊佔用,則重新分配一塊空間。

3. 根據Section頭部的信息,把文件的各個Section映射到分配的空間,並根據各個Section定義的數據來修改所映射的頁的屬性。

4. 如果文件被加載的地址不是ImageBase定義的地址,則重新修正ImageBase。

5. 根據PE文件的輸入表加載所需要的DLL到進程空間。

6. 然後替換IAT表內的數據爲實際調用函數的地址。

7. 根據PE頭內的數據生成初始化的堆和棧。

8. 創建初始化線程,開始運行進程。

這裏要提的是加載PE文件所需DLL的過程是建立在六個底層的API上。

LdrpCheckForLoadedDll:檢查要加載的模塊是否已經存在。

LdrpMapDll:映射模塊和所需信息到內存。

LdrpWalkImportDescriptor:遍歷模塊的輸入表來加載其所需的其他模塊。

LdrpUpdateLoadCount:計數模塊的使用次數。

LdrpRunInitializeRoutines:初始化模塊。

LdrpClearLoadInProgress:清楚某些標誌,表明加載已經完成。

十、 插入代碼到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字段。

  • 運行修改完的程序。

發佈了75 篇原創文章 · 獲贊 5 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章