学PE文件结构之记笔记
PE
- PE(protable executable) 可移植的可執(zhí)行文件
EXE和DLL文件之間的區(qū)別是語義上的,他們使用完全相同的PE格式。唯一的區(qū)別:用一個字段標識出EXE或DLL。
64位Windows,新的PE : PE32+。普通PE:PE32.PE32+沒有任何新的結(jié)構(gòu)加進去。
PE格式定義的主要地方位于頭文件 winnt.h,頭文件中幾乎能找到關(guān)于PE文件的所有定義。
文件內(nèi)容被分割為不同區(qū)塊。每個塊都有自己的屬性(是否只讀,是否有代碼,可讀/可寫)
PE文件不是作為單一內(nèi)存映射文件被裝入內(nèi)存。
PE裝載器遍歷PE,決定哪一部分被映射。映射方式:文件的較高偏移映射到內(nèi)存里較高的偏移地址
磁盤文件被裝入內(nèi)存,磁盤上的數(shù)據(jù)結(jié)構(gòu)布局和內(nèi)存上的一樣
基地址、相對虛擬地址、文件偏移地址
MS-DOS頭部
PE頭
在IMAGE_DOS_HEADER中的e_lfanew能找到PE頭的起始偏移量
IMAGE_NT_HEADERS
PEHeader是PE相關(guān)結(jié)構(gòu)NT映像頭(IMAGE_NE_HEADER) 的簡稱,包含許多PE裝載器用到的重要容器。
Signature:在有效的PE文件中,Signature字段置為00004550h,ASCII:“PE00",PE文件的開頭
IMAGE_FILE_HEADER
(6)SizeOfOptionalHeader: 緊跟著IMAGE_FILE_HEADER 后邊的數(shù)據(jù)結(jié)構(gòu)(IMAGE_OPTIONAL_HEADER)的大小。(對于32位PE文件,這個值通常是00E0h;對于64位PE32+文件,這個值是00F0h )。
IMAGE_OPTIONAL_HEADER32(可選頭
此結(jié)構(gòu)中大部分字段不重要,但是一些病毒會利用里面不重要的部分,在其中做手腳
+28h DWORD AddressOfEntryPoint // 程序執(zhí)行入口RVA
指出文件被執(zhí)行時的入口地址,(RVA地址)。如果在一個可執(zhí)行文件上附加了一段代碼,并想讓這一段代碼首先被執(zhí)行,只需要將這個入口地址指向附加的代碼就可以了。(可能會執(zhí)行惡意代碼)
對于一般DLL文件,一般不起作用,這塊被填充0
+34h DWORD ImageBase // 程序的首選裝載地址
程序的首選裝載地址,只有指定的地址被其他模塊使用時,文件被裝入到其他地址。
EXE文件,每個文件總是使用獨立的虛擬地址空間,優(yōu)先裝入的地址不可能被其他模塊占據(jù),EXE總是能按這個地址裝入。
DLL文件,多個DLL文件全部使用宿主EXE文件的地址空間,不能保證優(yōu)先裝入地址,所以包含重定位信息。
+38h DWORD SectionAlignment字段 //內(nèi)存中區(qū)塊的對齊大小(默認1000hB)
**和+3ch DWORD FileAlignment字段 ** // 文件中的區(qū)塊的對齊大小(默認200hB)
SectionAlignment字段:每個節(jié)裝入的地址時本字段指定數(shù)值的整數(shù)值。
FileAlignment字段:節(jié)存儲在磁盤文件中時的對其單位
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] //數(shù)據(jù)目錄表
數(shù)組,一直是16個元素。數(shù)組里面每一個都是一個結(jié)構(gòu)
IMAGE_DATA_DIRECTORY STRUCTVirtualAddress DWORD ; // 數(shù)據(jù)的起始RVAisize DWORD ; // 數(shù)據(jù)塊的長度 數(shù)組的十六個元素 0、導(dǎo)出表 1、導(dǎo)入表 2、資源 5、重定位表PE文件到內(nèi)存的映射
執(zhí)行PE文件的時候,Windows不在一開始就將整個文件讀入內(nèi)存。采用與內(nèi)存映射文件類似的機制
當(dāng)且僅當(dāng)真正執(zhí)行到某個內(nèi)存頁中的指令或者訪問某一頁中的數(shù)據(jù)時,這個頁才會被從磁盤提交到物理內(nèi)存。文件裝入的速度和文件大小沒有很大的關(guān)系。(don’t call me ,i will call you)
再裝載可執(zhí)行文件的時候,有些數(shù)據(jù)再裝入前會被預(yù)處理,如重定位等。
Windows裝載DOS、PE文件頭部分和節(jié)表部分時不進行任何特殊處理的,而再裝載節(jié)的時候會自動按節(jié)的屬性做不同的處理。
會處理以下幾個方面的內(nèi)容:
內(nèi)存頁的屬性
節(jié)的偏移地址
節(jié)在磁盤中的偏移和在內(nèi)存中的偏移是不同的。
節(jié)是相同屬性數(shù)據(jù)的組合。
節(jié)的尺寸
不進行映射的節(jié)
節(jié)表(區(qū)塊表)
索引的作用。所有節(jié)的屬性都被定義在節(jié)表中。節(jié)表在文件中的排列順序與它們描述的節(jié)在文件中的排列順序是一致的。
全部有效結(jié)構(gòu)的最后,以一個空的IMAGE_SECTION_HEADER結(jié)構(gòu)作為結(jié)束,所以節(jié)表中總的IMAGE_SECTION_HEADER結(jié)構(gòu)數(shù)量,等于節(jié)的數(shù)量加一。
IMAGE_SECTION_HEADER結(jié)構(gòu)的總數(shù) 由IMAGE_NT_HEADER->FileHeader.NumberOfSection 來指定。
節(jié)表總是被存放在緊接在PE文件頭的地方。
節(jié)表(區(qū)塊表)
一個區(qū)塊中的數(shù)據(jù)僅只是由于屬性相同而放在一起,并不一定是同一種用途的內(nèi)容。例如:輸入表、輸出表有可能和只讀常量一起放在同一個區(qū)塊中,因為他們的屬性都是可讀不可寫的。
每個區(qū)塊表的大小是 28h。
成員:
Name:區(qū)塊名。由8位的ASCII碼組成。如果區(qū)塊名超過8個字節(jié),沒有最后的終止標志“NULL”。
定義的區(qū)塊名是唯一的。病毒感染文件時,要開辟一塊區(qū)塊“.kkkkfj",再次感染時可以判斷是不是感染過。
區(qū)塊名可任意定義。從PE文件中讀取需要的區(qū)塊的時候,不能以區(qū)塊的名稱作為定位的標準和依據(jù)。正確方法:按照IMAGE_OPTIONAL_HEADER32結(jié)構(gòu)中的數(shù)據(jù)目錄表(IMAGE_DATA_DIRECTORY )結(jié)合進行定位
Virtual Size:對應(yīng)的區(qū)塊的大小,是區(qū)塊的數(shù)據(jù)在沒有進行對齊處理的大小
Virtual Address:該區(qū)塊裝載到內(nèi)存中的RVA地址,按照內(nèi)存頁對齊。數(shù)值總是SctionAlignment的整數(shù)倍。默認1000h
SizeOfRawData:該區(qū)塊在磁盤中所占的大小。
PointerToRawData:該區(qū)塊在磁盤中的偏移(從文件頭開始算起的偏移量
第一個區(qū)塊的 PointerToRawData+第一個區(qū)塊的 SizeOfRawData = 第二個區(qū)塊的起始位置
Characteristics:區(qū)塊的屬性(可查
PE文件一般至少會有兩個區(qū)塊,代碼塊、數(shù)據(jù)塊。
在c++里命名區(qū)塊:用# pragma來聲明,格式:# pragma data_msg("FC_data") .
#為宏處理符號
對齊值
RVA和文件偏移的轉(zhuǎn)換
輸入表(導(dǎo)入表) IMAGE_DIRECTORY_ENTRY_IMPORT
輸入函數(shù)
被程序調(diào)用但其執(zhí)行代碼又不在程序中的函數(shù)。這些函數(shù)的代碼位于相關(guān)的DLL文件中。
對于磁盤上的PE文件,它無法得知這些輸入函數(shù)在內(nèi)存中的地址,只有當(dāng)PE文件被裝入內(nèi)存后,windows加載器才將相關(guān)的DLL裝入,并將調(diào)用輸入函數(shù)的指令和函數(shù)實際所處的地址聯(lián)系起來。即”動態(tài)鏈接”。
輸入表是以一個IMAGE_IMPORT_DESCRIPTOR(IID)的數(shù)組開始。
每個被PE文件鏈接進來的DLL文件都分別對應(yīng)一個IID數(shù)組結(jié)構(gòu)。在這個IID數(shù)組中,沒有指出又多少個項,但它最后是以一個 全為NULL(0) 的IID作為結(jié)束的標志。
IID結(jié)構(gòu)定義:
IID成員:
補充:
- IMAGE_THUNK_DATA
判斷按名字導(dǎo)入還是按序號導(dǎo)入:
Ordinal最高位為1:按序號,低16位就是導(dǎo)入序號
最高位為0:AddressOfData是一個RVA,指向一個IMAGE_IMPORT_BY_NAME結(jié)構(gòu),用來保存名字信息。
-
IMAGE_IMPORT_BY_NAME
typedef struct _IMAGE_IMPORT_BY_NAME{WORD Hint;CHAR Name[1]; }IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;前兩個字節(jié):Hint,為函數(shù)在地址表的索引(沒多大用)
第三個字節(jié):Name[1],為函數(shù)名字的開始。
OriginalFirstThunk:指向INT表(IMAGE_THUNK_DATA結(jié)構(gòu))
IMAGE_THUNK_DATA32結(jié)構(gòu)為4字節(jié)大小,如果這個值的最高位(32位)為1,那么剩下的31位為所需函數(shù)的導(dǎo)出序號(這個函數(shù)在dll中是以序號導(dǎo)出的
如果為0,值是IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的RVA
Name:DLL文件的名字RVA
FirstThunk:指向IAT表(IMAGE_THUNK_DATA結(jié)構(gòu))
-
一般情況下,在程序加載前IAT表的值與INT表一樣(即都為IMAGE_THUNK_DATA結(jié)構(gòu))。在加載后,IAT表的值為函數(shù)的地址。
加載時,通過INT表找到第一個函數(shù)的名字,之后通過GetProcAddress得到函數(shù)的地址,寫入到IAT表同樣的索引的位置。然后找第二個函數(shù)的地址,遍歷完INT表,IAT表的地址也就寫入完成了。
-
另一種情況,IAT表在加載前就直接寫成了絕對地址。所需DLL的ImageBase設(shè)置成不同的值,這個表即課在程序加載前直接寫入。
通過_IAMGE_IMPORT_DESCRIPTOR的TimeDtaeStamp(時間戳)可以判斷導(dǎo)入表是否被綁定。
時間戳為0時,未綁定
為-1時,被綁定。
OriginalFirstThunk,指向IMAGE_THUNK_DATA數(shù)組,包含導(dǎo)入信息,在這個數(shù)組中只有Ordinal和AddressOfData是有用的,所以可以通過OriginalFirstThunk查找到函數(shù)的地址。
FirstThunk,在PE文件加載以前,它所指向的數(shù)組與OriginalFirstThunk中的數(shù)組不是同一個,但是內(nèi)容是相同的,都包含了導(dǎo)入信息,在加載之后,FirstThunk中的Function開始生效,它指向?qū)嶋H的函數(shù)地址,因為FirstThunk實際上指向IAT中的一個位置,IAT就充當(dāng)了IMAGE_THUNK_DATA數(shù)組,加載完成后,這些IAT就變成了實際的函數(shù)地址,即Function的意義。
程序加載前與程序加載后,輸入表所指內(nèi)容的變化。
- 程序加載前
輸入地址表(IAT)
程序加載前,兩個并行的指針(OriginalFirstThunk和FirstThunk)同時指向IMAGE_IMPORT_BY_NAME結(jié)構(gòu)。
第一個數(shù)組(由OriginalFirstThunk指向)是單獨的一項,而且不能被改寫,稱為INT。
第二個數(shù)組(由FirstThunk指向)事實上是由PE裝載器重寫的。
PE裝載器的核心操作:
PE裝載器首先搜索OriginalFirstThunk,找到之后加載程序迭代搜索數(shù)組中的每個指針,找到每個IMAGE_IMPORT_BY_NAME結(jié)構(gòu)所指向的輸入函數(shù)的地址,然后加載器用函數(shù)真正入口地址來替代由FirstThunk數(shù)組中的一個入口,因此我們稱為輸入地址表(IAT)。
脫殼中,IAT修復(fù)原理
導(dǎo)入地址表(IAT):Import Address Table。由于導(dǎo)入函數(shù)就是被程序調(diào)用但其執(zhí)行代碼又不在程序中的函數(shù),這些函數(shù)的代碼位于一個或者多個DLL中,當(dāng)PE文件被裝入內(nèi)存的時候,Windows裝載器才將DLL裝入,并將調(diào)用導(dǎo)入函數(shù)的指令和函數(shù)實際所處的地址聯(lián)系起來(動態(tài)鏈接),這操作就需要導(dǎo)入表。
修復(fù)IAT原理:
程序的IAT是連續(xù)排列的,只需要找到IAT的起始位置和末位置,就可以確定IAT的地址和大小。在壓縮殼中,只要找到一個調(diào)用系統(tǒng)的API的call地址,然后在數(shù)據(jù)窗口中查找,確定IAT起始和結(jié)束地址。然后在OD中手動修復(fù)。
為什么要手動修復(fù):
IAT主要用于DLL文件的重定位,IAT的引入相較于16位dos程序不再需要包含庫文件,而是通過表的形式進行映射。如果IAT不準確,則程序無法執(zhí)行相關(guān)庫的函數(shù)。
壓縮殼對節(jié)區(qū)進行了壓縮,把IAT修改為殼自身的IAT,在解壓縮的最后一步會還原IAT使程序可以正常運行,所以脫殼后需要進行IAT的修復(fù)。
相較于加殼程序的IAT,運行加殼程序后真正的IAT要多得多。如果只進行脫殼,而不進行IAT的修復(fù),程序的IAT是被損壞的。
脫殼程序無法在WIN7以上平臺運行
windows win7系統(tǒng)開始使用ASLR技術(shù)防止溢出攻擊。使得每次加載程序都加載到一個隨機的虛擬地址。ASLR依賴于重定位表進行定位,對于EXE程序來說,重定位是可選的,通過關(guān)閉ASLR即可解決。
輸出表(導(dǎo)出表)
導(dǎo)出表的定義
typedef struct _IMAGE_EXPORT_DIRECTORY{DWORD Characteristics;//現(xiàn)在沒有用到,一般為0DWORD TimeDateStamp;//導(dǎo)出表生成的時間戳WORD MajorVersion; // 版本。實際沒用。WORD MinorVersion; // 和上面一樣。都是0DWORD Name; // 模塊的真實名稱DWORD Base; // 序號的基數(shù)DWORD NumberOfFunctions;// 所有導(dǎo)出函數(shù)的數(shù)量DWORD NumberOfNames;// 按名字導(dǎo)出函數(shù)的數(shù)量DWORD AddressOfFunctions; // 這三個看下面DWORD AddressOfNames;DWORD AddressOfNameOrdinals; }擴展名為**.exe的PE文件中一般不存在**導(dǎo)出表,而大部分的.dll文件都存在導(dǎo)出表。
導(dǎo)出表中的主要成分是一個表格,內(nèi)含函數(shù)名稱、輸出序數(shù)等。序數(shù)是指定DLL中某個函數(shù)的16位數(shù)字,在所指向的DLL文件中是獨一無二的。
數(shù)據(jù)目錄表(IMAGE_DATA_DIRECTORY )的第一個成員指向?qū)С霰?#xff0c;是一個IMAGE_EXPORT_DIRECTORY(簡稱IED)結(jié)構(gòu)
IED結(jié)構(gòu)中有意義的字段:
Name:模塊的真實名稱。改不掉。
Base:序號的基數(shù),按序號,導(dǎo)出函數(shù)的序號值從Base開始遞增
NumberOfFunctions:導(dǎo)出函數(shù)的總數(shù)
NumberOfNames:以名稱方式導(dǎo)出的函數(shù)的總數(shù)
AddressOfFunctions:指向輸出函數(shù)地址的RVA,指向一個DWORD數(shù)組,數(shù)組中的每一項是一個導(dǎo)出函數(shù)的RVA,順序與導(dǎo)出序號相同。
AddressOfNames:指向輸出函數(shù)名字的RVA,指向函數(shù)名的字符串地址表----一個雙字(DWORD)數(shù)組,數(shù)組中的每一項指向一個函數(shù)名稱字符串的RVA。
AddressOfNameOrdinals:指向輸出函數(shù)序號的RVA。指向一個word類型的數(shù)組(不是雙字)。數(shù)組項目與文件名地址表(AddressOfNames)中的項目一一對應(yīng),表示該名字的函數(shù)在AddressOfFunctions中的序號。
對(7.)舉例:
加入函數(shù)名稱的字符串地址表(AddressOfNames)的第n項指向一個字符串“myfunction”,可以去查找AddressOfNameOrdinals指向的數(shù)組的第n項,假如第n項中存放的值為x,那么,AddressOfFunctions字段描述的地址表中的第 x 項函數(shù)入口地址,對應(yīng)的函數(shù)名稱就是“myfunction”。
看圖加強理解:
AddressOfNames 指向一個數(shù)組,數(shù)組里保存著一組 RVA,每個RVA指向一個字符串,這個字符串即導(dǎo)出的函數(shù)名,與這個函數(shù)名對應(yīng)的是AddressOfNameOrdinals中的對應(yīng)項。獲取導(dǎo)出函數(shù)地址時,先在AddressOfNames中找到對應(yīng)的名字。
比如Func2,他在AddressOfNames中是第二項,然后從AddressOfNameOrdinals中取出第二項的值,這里是2,表示函數(shù)入口保存在AddressOfFunctions這個數(shù)組中下標為2的項里,即第三項,取出其中的值,加上模塊基地址便是導(dǎo)出函數(shù)的地址。如果函數(shù)是以序號導(dǎo)出的,那么查找的時候直接用序號減去Base,得到的值就是函數(shù)在AddressOfFunctions中的下標。
為啥有三個RVA?
導(dǎo)出表是用來描述模塊(dll)中的導(dǎo)出函數(shù)的結(jié)構(gòu),如果一個模塊導(dǎo)出了函數(shù),那么這個函數(shù)會被記錄在導(dǎo)出表中,這樣通過GetProcAddress函數(shù)就能動態(tài)獲取到函數(shù)的地址。
函數(shù)導(dǎo)出的方式:
按序號導(dǎo)出
按名字導(dǎo)出
從序號查找函數(shù)入口地址
Windows裝載器的工作步驟:
在GetProcAddress()里,如果以序號索引,要減去base才是數(shù)組里面的索引值。
Windows裝載器的工作步驟:
一般情況下,病毒通過函數(shù)名稱查找入口地址,因為病毒程序是作為一段額外的代碼被附加到可執(zhí)行文件中的,如果病毒代碼中用到某些API的話,這些API的地址不可能在宿主文件的導(dǎo)出表中為病毒代碼準備好。因此只能通過在內(nèi)存中動態(tài)查找的方法來實現(xiàn)獲取API的地址。
重定位表
重定位表在病毒研究方面起重要作用。
基址重定位
重定位就是程序理論上要占據(jù)這個地址,但是由于某種原因,這個地址現(xiàn)在不能讓這個程序占,必須轉(zhuǎn)移到別的地址,就需要基址重定位。
為什么需要基址重定位
補充知識:動態(tài)鏈接庫它自己是沒有占據(jù)任何私有空間的,都是寄生在應(yīng)用程序的私有空間里面。
- 舉例:
test.exe可執(zhí)行程序需要三個動態(tài)鏈接庫dll(a.dll,b.dll,c.dll),假設(shè):test.exe的ImageBase為400000H,而三個dll的基址ImageBase均為1000000H。
那么OS的加載程序在將test.exe加載進內(nèi)存時,直接復(fù)制其程序到400000H開始的虛擬內(nèi)存,接著再加載三個dll:假設(shè)按abc的順序加載,如果test.exe的ImageBase+SizeOdImage+1000H不大于1000000H,則a.dll直接復(fù)制到1000000H開始的內(nèi)存中;
b.dll加載時,雖然基址也為1000000H,但是由于已經(jīng)被a.dll占用,則b.dll需要重新分配基址。比如加載程序經(jīng)過計算將其分配到1200000H的地址,c.dll同樣經(jīng)過計算將其加載到1500000H的地址。
但是b.dll和c.dll有些地址時根據(jù)ImageBase固定的,被寫死了的,而且是絕對地址不是相對偏移地址。
比如b.dll中存在一個call 0x01034560,這是一個絕對地址,相對于ImageBase的地址為0x34560H;但是此時的內(nèi)存中b.dll存在的地址時1200000H開始的內(nèi)存,加載器分配的ImageBase和b.dll中原來默認的ImageBase(1000000H)相差了200000H,因此該call值也應(yīng)該加上這個差值,被修正為0x1234560H,也就是相對偏移地址沒有改變。否則call的地址不修正會導(dǎo)致call指令跳轉(zhuǎn)的地址不是實際要跳轉(zhuǎn)的地址,獲取不到正確的函數(shù)指令,程序則不能正常運行。
由于一個dll中的需要修正的地址不止一兩個,可能有很多,所以用一張表記錄那些“寫死”的地址,將來加載進內(nèi)存時,可能需要一一修正,這張表稱為重定位表。
圖:(方便理解
(來源于網(wǎng)路)
一般每個PE文件都有一個重定位表。當(dāng)加載器加載程序時,如果加載器為某PE(.exe,.dll)分配的基址與其自身默認記錄的ImageBase不相同,那么該程序文件加載完畢后需要修正重定位表中的所有需要修正的地址。
如果相同則不需要修正,重定位表對于該dll也是沒有用的。
比如上面例子中的a.dll (由于一般情況.exe運行時被第一個加載,所以exe文件一般沒有重定位表,但是不代表所有exe都沒有重定位表)。 同理如果先加載b.dll后加載a.dll、c.dll,那么b.dll的重定位表就不起作用了。
但凡涉及到直接尋址的指令都需要進行重定位處理。
直接尋址----只要在機器碼中看到有地址的,就叫直接尋址。
間接尋址----地址被間接的保存起來,例如存放在寄存器中,然后通過方可寄存器來獲取地址。
補充知識。
“(機器碼)10001038 E8CFFFFFFF (匯編指令)call 1000100C”
為什么后邊顯示的是call+地址,而機器碼卻不包含地址信息?
有一種**”地址+偏移“**的形式。CFFFFFFFh事實上是一個偏移地址,小端序(little-edition),轉(zhuǎn)換過來就是FFFFFFCFh,也就是等于-31h。1000103Dh-31h == 1000100Ch。
系統(tǒng)對一條指令進行重定位需要哪些信息
重定位的算法可以總結(jié)為:將直接尋址指令中的雙字地址加上模塊的實際裝入地址與模塊建議裝入地址之差
重定位需要三個因素:
建議裝入的地址在PE文件頭中已經(jīng)定義了
實際裝入的地址在沒有被裝載器裝入前我們不能曉得,
所以,PE文件的重定位表(Baese Relocation Table)中保存 的是文件中所有需要進行重定位修正的代碼的地址。
Windows采用分組的方式,按照重定位項所在的頁面分組,每組保存一個頁面起始地址的RVA,頁內(nèi)的每項重定位項使用一個WORD保存重定位項在頁內(nèi)的偏移,縮小了重定位表的大小。
重定位表的定義:
typedef struct _IMAGE_BASE_RELOCATION{DWORD VirtualAddress;DWORD SizeOfBlock; // DWORD TypeOffset[1]; }IMAGE_BASE_RELOCATION;VirtualAddress:頁的起始地址RVA
SizeOfBlock:表示該分組保存了幾項重定位項。
一個重定位表由多個大小SizeOfBlock的Block組成,(不同塊的SizeOfBlock大小不一)。
TypeOffset:一個數(shù)組,數(shù)組每項大小為兩個字節(jié)(16位)由高4位和低12位組成。
高4位代表重定位類型,低12位是重定位地址(下面解釋
它與VirtalAddress相加即是指向PE映像中需要修改的那個代碼的地址。事實上,Windows只用了一種類型IMAGE_REL_BASED_HIGHLOW ,數(shù)值是3。
重定位表中的Block塊記錄了 1000H(4KB)大小 的內(nèi)存中需要重定位信息的地址(一頁大小),這些地址以VirtualAddreess為該頁的基址,偏移地址占兩個字節(jié)(1000H最多需要12bit即可:0~FFFH)。所以兩個字節(jié)的低12位為偏移地址。
而高4為就是一個標記,此標記為0011(3)時低12位才有效,否則該兩個字節(jié)可能是為了對齊而產(chǎn)生的。而且為對齊而產(chǎn)生的字節(jié)其值全為0.
圖:
是的網(wǎng)上拿的(還有水印emoji
圖的最下面全是0,表示重定位表結(jié)束。
總結(jié):哪些項目需要被重定位呢?
延遲導(dǎo)入表(Delay Import)
這種導(dǎo)入機制導(dǎo)入其他DLL的時機比較“遲”,因為有些導(dǎo)入函數(shù)可能使用的頻率比較低,或者在某些特定的場合才會用到,而有些函數(shù)可能要在程序運行一段時間吼才會用到,這些函數(shù)可以等到他實際使用的時候再去加載對應(yīng)的DLL,而沒必要在程序一裝載就初始化好。
延遲導(dǎo)入表(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT)
IMAGE_DATA_DIRECTORY.VirtualAddress 指向延遲導(dǎo)入表的起始地址。
延遲導(dǎo)入表的每一項都是一個ImgDelayDescr結(jié)構(gòu)體,和導(dǎo)入表一樣,每一項都代表一個導(dǎo)入的DLL
定義:
[cpp] view plaincopytypedef struct ImgDelayDescr { DWORD grAttrs; //區(qū)分版本 RVA rvaDLLName; //指向?qū)隓LL名字的RVARVA rvaHmod; //指向?qū)隓LL的模塊基地址的RVA。在DLL被真正導(dǎo)入前是NULL,導(dǎo)入后是實際的基地址。RVA rvaIAT; // 表示導(dǎo)入函數(shù)表,實際上是指向IAT的RVA。 DLL加載前,IAT里存放的是一小段代碼的地址,加載后才是真正的導(dǎo)入函數(shù)地址。 RVA rvaINT; //指向?qū)牒瘮?shù)的名字表的RVARVA rvaBoundIAT; RVA rvaUnloadIAT; //延遲導(dǎo)入函數(shù)卸載表。 DWORD dwTimeStamp; //時間戳} ImgDelayDescr, * PImgDelayDescr; typedef const ImgDelayDescr * PCImgDelayDescr;延遲導(dǎo)入表的處理。再延遲導(dǎo)入函數(shù)指向的IAT里,默認保存的是一段代碼的地址(rvaIAT),當(dāng)程序第一次調(diào)用到這個延遲導(dǎo)入函數(shù)時,流程會走到那段代碼,這段代碼的作用是啥嘞?
延遲導(dǎo)入函數(shù)的栗子:
[cpp] view plaincopy.text:75C7A363 __imp_load__InternetConnectA@32: ; InternetConnectA(x,x,x,x,x,x,x,x) .text:75C7A363 mov eax, offset __imp__InternetConnectA@32 .text:75C7A368 jmp __tailMerge_WININET兩行匯編,第一行把導(dǎo)入函數(shù)IAT項的地址放在eax中,然后用一個jmp跳轉(zhuǎn),跳轉(zhuǎn)的地址:
[cpp] view plaincopy__tailMerge_WININET proc near .text:75C6BEF0 push ecx .text:75C6BEF1 push edx .text:75C6BEF2 push eax .text:75C6BEF3 push offset __DELAY_IMPORT_DESCRIPTOR_WININET .text:75C6BEF8 call __delayLoadHelper .text:75C6BEFD pop edx .text:75C6BEFE pop ecx .text:75C6BEFF jmp eax .text:75C6BEFF __tailMerge_WININET endp其中,push了一個__DELAY_IMPORT_DESCRIPTOR_WININET,也就是ImgDelayDescr結(jié)構(gòu),他的DLL名字時wininet.dll。之后,**CALL了一個__delayLoadHelper,**在這個函數(shù)里,執(zhí)行了,加載DLL,查找導(dǎo)出函數(shù),填充導(dǎo)入表等一系列操作,函數(shù)結(jié)束時IAT中已經(jīng)是真正的導(dǎo)入函數(shù)的地址,這個函數(shù)同時返回了導(dǎo)入函數(shù)的地址,這個函數(shù)同時返回了導(dǎo)入函數(shù)的地址,所以之后的eax里保存的就是函數(shù)地址,最后的jmp eax就跳轉(zhuǎn)到了真實的導(dǎo)入函數(shù)中。
__delayLoadHelper:延遲加載DLL。它的參數(shù)中只有IAT項的偏移和整個模塊的延遲導(dǎo)入描述 DELAY_IMPORT_DESCRIPTOR_WININET ,但是參數(shù)中并沒有要導(dǎo)入函數(shù)的名字。
DELAY_IMPORT_DESCRIPTOR_WININET 中含有名字表,但是這個表里存的是所有要從該模塊導(dǎo)入的函數(shù)名字,不是“當(dāng)前”這個被調(diào)用函數(shù)的函數(shù)名。所以上面的“名字”不是 名字表中的。
Windows是如何得到名字的?
MS使用了一個巧妙的方法: __DELAY_IMPORT_DESCRIPTOR_WININET 中有一項是rvaIAT,這個實際上就是指向了IAT,而且是該模塊第一個導(dǎo)入函數(shù)的IAT的偏移。現(xiàn)在有兩個偏移:即將導(dǎo)入的函數(shù)IAT項的偏移(RVA1)和要導(dǎo)入模塊第一個函數(shù)IAT項的偏移(RVA0),(RVA1-RVA0)/4 = 導(dǎo)入函數(shù)IAT項再rvaIAT中的下表。rvaINT中的名字順序與rvaIAT中的順序是相同的,所以下標也相同,這樣就能獲取到導(dǎo)入函數(shù)的名字了。
有了函數(shù)名和模塊名,用GetProcAddress就能獲取到導(dǎo)入函數(shù)的地址了。
圖:
注意:
資源表
Windows將程序的各種界面定義為資源。
包括加速鍵(Accelerator)、位圖(Bitmap)、光標(Cursor)、對話框(Dialog Box)、圖標(Icon)、菜單(Menu)、串表(String Table)、工具欄(Toolbar)和版本信息(Version Information)等。
這些內(nèi)容或界面元素,都以二進制的形式保存在PE文件中。
這些數(shù)據(jù)保存的位置,就是PE文件的資源段(.rsrc)
這些數(shù)據(jù)的組織格式,稱為資源表
資源表的定位
位置:NT頭–>擴展頭–>資源目錄表–>第三個元素–>相對虛擬地址(RVA)
資源結(jié)構(gòu)
幾乎所有的PE文件中都包含著資源,與導(dǎo)入表和導(dǎo)出表相比,資源的組織方式要復(fù)雜很多
資源表的結(jié)構(gòu)比較復(fù)雜,一共有三層,三層從上到下是樹狀擴展的
三層:資源類型 -> 資源ID -> 資源代碼頁
圖:
資源目錄結(jié)構(gòu)
數(shù)據(jù)目錄表中的IMAGE_DIRECTORY_ENTRY_RESOURCE條目(第三項)包含資源的RVA和大小。資源目錄結(jié)構(gòu)中的每一個節(jié)點都是由IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)和緊跟其后的幾個IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)組成。(有點類似套娃)
圖:
注意: NumberOfNamedEntries和NumberOfIdEntries,說明了本目錄中目錄項目的數(shù)量。兩者加起來是本目錄中目錄項的總和。也就是后面跟著的IMAGE_RESOURCE_DIRECTORY_ENTRY的數(shù)目。
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {union {struct {DWORD NameOffset:31;DWORD NameIsString:1;};DWORD Name; WORD Id;}DUMMYUNIONNAME; // 資源名稱union {DWORD OffsetToData;struct {DWORD OffsetToDirectory:31; DWORD DataIsDirectory:1;}DUMMYSTRUCTNAME2;}DUMMYSTRUCTNAME2;// 資源位置 } IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;IMAGE_RESOURCE_DIRECTORY_ENTRY一共8字節(jié),里面包含兩個聯(lián)合體,每個聯(lián)合體4字節(jié)
第一個聯(lián)合體:資源的名稱
第二個聯(lián)合體:資源的位置
**第一個聯(lián)合體:**如果最高位是0,也就是NameIsString為0,此時4字節(jié)代表資源類型,也就是ID起作用。
| 0x01 | 鼠標指針 | 0x08 | 字體 |
| 0x02 | 位圖 | 0x09 | 快捷鍵 |
| 0x03 | 圖標 | 0x0A | 非格式化資源 |
| 0x04 | 菜單 | 0x0B | 消息列表 |
| 0x05 | 對話框 | 0x0C | 鼠標指針組 |
| 0x06 | 字符串列表 | 0x0E | 圖標組 |
| 0x07 | 字體目錄 | 0x10 | 版本信息 |
如果NameIsString為1,NameOffset指向保存字符串的結(jié)構(gòu)體。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {WORD Length;WCHAR NameString[ 1 ]; }IMAGE_RESOURCE_DIR_STRING_U,*PIMAGE_RESOURCE_DIR_STRING_U;第二個元素NameString為字符串起始,長度為Length,這個串不是以0結(jié)尾
**第二個聯(lián)合體:**如果最高位為1,即DataIsDirectory為1,OffsetToDirectory指向的地方是一個目錄。
通常,第一層和第二層,這個值都是1
若DataIsDirectory為0, OffsetToDirectory指向的地方是一個數(shù)據(jù)
通常,第三層,這個值為0
1.第一層
2.第二層
3.第三層
與前兩層一樣,起始于。。頭,緊接著。。數(shù)組,但是數(shù)組個數(shù) = 1.
使用的是Name與OffsetToData,分別代表了資源語言類型與資源數(shù)據(jù)相對地址。Name是指語言內(nèi)碼,比如936代表簡體中文。
OffsetToData是相對整個資源結(jié)構(gòu)的偏移地址,指向一個IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu)體,該結(jié)構(gòu)體定義如下:
聽小甲魚的網(wǎng)課記的筆記,也有大部分借鑒于其他師傅博客,很多。
本文中的圖全部來源于網(wǎng)絡(luò)。
主要借鑒的博客鏈接貼一下:
PE文件結(jié)構(gòu)
延遲導(dǎo)入表
等等。
總結(jié)
以上是生活随笔為你收集整理的学PE文件结构之记笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。