日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

PE 格式详解

發布時間:2024/4/19 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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 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選項設置。

數據位在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字段。

  • 運行修改完的程序。

  • ====================

  • PE文件基礎補注

    關鍵詞:?PE文件??地址轉換?IAT??IMAGE_IMPORT_BY_NAME

    前言:?最近學習PE,?略有心得,?拿來和大家分享.?

    感謝:?小蝦斑斑,?非安全??,Bookworm對我的幫助.

    附件:pe.rar?

    1.IMAGE_SECTION_HEADER小結:
    ??
    1.1???獲得節表數?:NumberOfSections?=?NtHeader->FileHeader.NumberOfSections;

    1.2???節表獲得方法
    ??????
    ??????方法1.因為NT頭之后就是節表,故,節表頭地址就是nt頭地址加上NT結構大小.
    ??????SectionHeader=(PIMAGE_SECTION_HEADER)((UINT32)NtHeader+(UINT32)(sizeof(IMAGE_NT_HEADERS)));
    ??
    ??????方法2.或者用ImageBase+SizeOfHeaders的辦法直接定位.
    ??????SectionHeader=(PIMAGE_SECTION_HEADER)((UINT32)(NtHeader->OptionalHeader.ImageBase)+
    ??????????????????????????????????????????(UINT32)(NtHeader->OptionalHeader.SizeOfHeaders));
    ??
    ??????方法3.既然節都是連在一起的,那么,也就可以這樣:?
    ??????????????SectionHeader=?(PIMAGE_SECTION_HEADER)?(NtHeader?+?1),
    ??
    ??????方法4.論壇里面?hmimys?告訴的辦法:????
    ??????????????SectionHeader=(PIMAGE_SECTION_HEADER)((UINT32)NtHeader+0x18+
    ???????????????????????????????????????????????????(UINT32)(NtHeader->FileHeader.SizeOfOptionalHeader));
    ??????????????到現在我還沒有弄懂為什么?hmimys?說最好要用方法4而不用方法3.
    ????
    ????
    2.?IMAGE_IMPORT_DECSRITOR?小結:

    2.1:獲得引入表結構起始地址:
    ?????
    ?????方法1:ImportDec?=?(PIMAGE_IMPORT_DESCRIPTOR)(NtHeader->OptionalHeader.DataDirectory[12].VirtualAddress);
    ????????????這個方法我覺得理論上是對的,但是我在運行的時候總是得不到正確的地址.后來知道,似乎不能用'12',而要用IMAGE_DIRECTORY_ENTRY_IAT這個宏

    ?????方法2:ImportDes?=?(PIMAGE_IMPORT_DESCRIPTOR)((DWORD)(NtHeader->OptionalHeader.DataDirectory)+
    ????????????????????????????????????????????????????(DWORD)(sizeof(IMAGE_DATA_DIRECTORY)*12));
    ???
    ?????方法3?:?ImportDes?=?(PIMAGE_IMPORT_DESCRIPTOR)(NtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress-
    ????????????????Offset?+?(PBYTE)pMapping);

    ?????注?:?前兩種方法都是從?IAT?中得出?IMAGE_IMPORT_DESCRIPTOR,而后面的那個是?非安全?大哥教的.?這里有個疑問:?
    ??????????3種方法都可以得到?IMAGE_IMPORT_DESCRIPTOR?結構,都可以得到函數名,?區別在于前兩種方法枚舉的函數名不全.?
    ??????????難道說兩個結構都指向同一個結構PIMAGE_IMPORT_DESCRIPTOR?
    ?
    2.2??IMAGE_IMPORT_DESCRIPTOR?結構既不是在Import?Symbols中,也不是在IAT?(IMAGE_IMPORT_ADDRESS_TABLE)中。它就是一個結構.?
    ?????我原來說:"IMAGE_IMPORT_DESCRIPTOR?結構不是在Import?Symbols中,是在IAT?(IMAGE_IMPORT_ADDRESS_TABLE)中。"?有問題.
    ?????就是因為這個錯誤的理解,?讓我走了好多死路.
    ?????
    ?????這個是Winnt.h中關于?IMAGE_SYNMBOL的結構信息

    ?????typedef?struct?_IMAGE_SYMBOL?{
    ??????union?{
    ????????BYTE????ShortName[8];
    ????????struct?{
    ????????????DWORD???Short;?????//?if?0,?use?LongName
    ????????????DWORD???Long;??????//?offset?into?string?table
    ????????}?Name;
    ????????PBYTE???LongName[2];
    ?????}?N;
    ?????DWORD???Value;
    ?????SHORT???SectionNumber;
    ?????WORD????Type;
    ?????BYTE????StorageClass;
    ?????BYTE????NumberOfAuxSymbols;
    ?????}?IMAGE_SYMBOL;
    ?????
    ?????typedef?IMAGE_SYMBOL?UNALIGNED?*PIMAGE_SYMBOL;

    ?????而下面的是IAT:?

    ?????typedef?struct?_IMAGE_IMPORT_BY_NAME?{
    ??????WORD????Hint;
    ??????BYTE????Name[1];
    ?????}?IMAGE_IMPORT_BY_NAME,?*PIMAGE_IMPORT_BY_NAME;

    ?????typedef?struct?_IMAGE_IMPORT_DESCRIPTOR?{
    ??????union?{
    ??????????DWORD???Characteristics;????????????//?0?for?terminating?null?import?descriptor
    ??????????DWORD???OriginalFirstThunk;?????????//?RVA?to?original?unbound?IAT?(PIMAGE_THUNK_DATA)
    ??????};
    ??????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;
    ??????DWORD???FirstThunk;?????????????????????//?RVA?to?IAT?(if?bound?this?IAT?has?actual?addresses)
    ?????}?IMAGE_IMPORT_DESCRIPTOR;
    ?????typedef?IMAGE_IMPORT_DESCRIPTOR?UNALIGNED?*PIMAGE_IMPORT_DESCRIPTOR;

    ?????_IMAGE_IMPORT_DESCRIPTOR?結構聯合中的OriginalFirstThunk?,?就是到IMAGE_THUNK_DATA的RVA.?
    ?????如果像下面這樣寫,也許更明白

    ?????typedef?struct?_IMAGE_THUNK_DATA?{
    ????????union?{
    ????????????PBYTE?ForwarderString;
    ????????????PDWORD?Function;
    ????????????DWORD?Ordinal;
    ????????????PIMAGE_IMPORT_BY_NAME?AddressOfData;
    ????????}?;
    ?????}?IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

    ?????typedef?struct?_IMAGE_IMPORT_DESCRIPTOR?{
    ????????union?{
    ????????????DWORD?Characteristics;
    ????????????PIMAGE_THUNK_DATA?OriginalFirstThunk;
    ????????}?;
    ????????DWORD?TimeDateStamp;
    ????????DWORD?ForwarderChain;
    ????????DWORD?Name;
    ????????PIMAGE_THUNK_DATA?FirstThunk;
    ?????}?IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
    ?????

    3.?地址轉換小結(RVAToOffset):?

    ???為什么要地址轉換,?前人的文章說了很多,下面給出我的轉換方法:??
    ???
    ???3.1?函數,它能給出RVA返回此RVA所在的節,來自?Matt?Pietrek的書:?

    ???PIMAGE_SECTION_HEADER?GetEnclosingSectionHeader(DWORD?rva){
    ????????unsigned?i;
    ??????PIMAGE_SECTION_HEADER?section?=?IMAGE_FIRST_SECTION32(NtHeader);
    ??????for?(?i=0;?i?<?NtHeader->FileHeader.NumberOfSections;?i++,section++){
    ???????????????if?(?(rva?>=section->VirtualAddress)?&&?
    ?????????????(rva?<?(section->VirtualAddress?+?section->Misc.VirtualSize)))
    ????????????return?section;
    ??????}
    ?????????return?0;?
    ???}

    ???注:?hnhuqiong?給的?ollydump300110?的源碼里面也有類似函數,但是,
    ???????很明顯的有漏洞,那就是若RVA不在任何一個Section那么函數會返回最后
    ???????一個Section,?而不是像這里返回?0?.下面是原始連接
    ???http://bbs.pediy.com/showthread.php?threadid=26520

    ???3.2?RVAToOffset:

    ???我一直沒有注意的就是'Offset'這個詞.?Offset其實還是一個偏移,只不過是
    ???在文件中,?要想得到目標文件的IAT,?就要將這個值加上由?MapViewOfFile?返回
    ???的文件基址指針.

    ???Offset的的獲得?:?
    ????????????pSection?=?GetEnclosingSectionHeader(NtHeader->OptionalHeader.DataDirectory???????????????????????????????????????

    ???????????????????????????????????????????[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress);
    ????????????Offset?=?(DWORD)?(pSection->VirtualAddress?-?pSection->PointerToRawData);
    ??
    ???以獲得IMAGE_THUNK_DATA結構為例,給出用法:?

    ????????ThunkData?=?(PIMAGE_THUNK_DATA)((DWORD)ImportDes->OriginalFirstThunk?-
    ???????????????????????????????????????Offset?+?(PBYTE)pMapping);
    ???呵呵,?(DWORD)ImportDes->OriginalFirstThunk?-Offset?得到的只是文件中的偏移,?
    ???注意加上由?MapViewOfFile?返回的pMapping.?如果你象我原來一樣,加上的是
    ???NtHeader->OptionalHeader.ImageBase?,?那么恭喜你,?訪問錯誤.????????????
    ??

    4.?用VC?6.0?+?API獲得IMAGE_IMPORT_BY_NAME結構的一點問題.

    ???在?VC?里面,??在一個結構指針比如ThunkData后面加上'->'時,?vc會自動的列出
    ???結構的成員供你選擇,?十分方便.?但是,?通過ThunkData繼續想獲得IMAGE_IMPORT_BY_NAME
    ???結構的時候,?你在ThunkData后面加'->'時,?出來的是一個'u1'.?此時不要疑惑,
    ???這個'u1'就是?IMAGE_THUNK_DATA?里面的那個?union?的名稱,?所以你可以這樣得到
    ???IMAGE_IMPORT_BY_NAME結構:?

    ???????ImportBN?=?(PIMAGE_IMPORT_BY_NAME)((DWORD)(ThunkData->u1.AddressOfData)-
    ????????????????????????????????Offset?+(PBYTE)pMapping);

    5.???Iczelion的PE教程關于導入表的描述沒有講清楚,只是說用IMAGE_THUNK_DATA
    ??????的每個數組元素和IMAGE_ORDINAL_FLAG32,比較可以推斷如果某個函數是由函數序數引出的,
    ??????我就誤解成用ImportDes->OriginalFirstThunk或者ImportDes->FirstThunk?判斷。是不是錯的很遠?
    ??????參考(【翻譯】“PE文件格式”1.9版?完整譯文(附注釋))http://bbs.pediy.com/showthread.php?threadid=21932,
    ??????我們應該用IMAGE_THUNK_DATA結構里面的AddressOfData來判斷。下面的代碼可行:

    ??????while(ThunkData->u1.AddressOfData!=NULL){
    ?????????ImportBN?=?(PIMAGE_IMPORT_BY_NAME)((DWORD)(ThunkData->u1.AddressOfData)?-?Offset?+(PBYTE)pMapping);?
    ???//顯示導入函數
    ???if(((DWORD)ThunkData->u1.AddressOfData?&?IMAGE_ORDINAL_FLAG32)?==?0){
    ????AddText(hEdit,TEXT("%03d:??%s\r\n"),i++,ImportBN->Name);
    ???}
    ???else{
    ????AddText(hEdit,TEXT("%03d:??Ord?by?Hint\r\n"),i++);
    ???}
    ???ThunkData?++;??
    ??????}//End?of?while

    6??????導出表:
    6.1????導出表的結構,
    ???typedef?struct?_IMAGE_EXPORT_DIRECTORY?{
    ??????DWORD???Characteristics;
    ??????DWORD???TimeDateStamp;
    ??????WORD????MajorVersion;
    ??????WORD????MinorVersion;
    ??????DWORD???Name;
    ??????DWORD???Base;
    ??????DWORD???NumberOfFunctions;
    ??????DWORD???NumberOfNames;
    ??????DWORD???AddressOfFunctions;?????//?RVA?from?base?of?image
    ??????DWORD???AddressOfNames;?????????//?RVA?from?base?of?image
    ??????DWORD???AddressOfNameOrdinals;??//?RVA?from?base?of?image
    ???}?IMAGE_EXPORT_DIRECTORY,?*PIMAGE_EXPORT_DIRECTORY;

    6.2???AddressOfNames?和AddressOfNameOrdinals?是一一對應的,只不過一個用于名字,
    ??????一個用于序號,?同一個函數的索引都相同。

    6.3???NumberOfFunctions?–?NumberOfNames?應該就是由序號引出的函數數目了

    6.4???對于由序號導出的函數,不知道有沒有辦法能通過序數找到函數名。個人考慮似乎不可能這樣
    ??????找函數名字,不然,微軟未公開的函數就都被我們通過函數序數枚舉出來了??:)

    7:??把我的PE查看器修改了下,?原來的在處理用序號引出的函數時會出錯.:)

總結

以上是生活随笔為你收集整理的PE 格式详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。