PE檔案與殼
- 一、PE檔案的載入機制:
- (一)相對虛擬地址RVA:
- (二)檔案偏移地址(物理地址):
- 二、PE檔案結構簡述
- (一)MS-DOS頭部
- (二)PE檔案頭
- ——DataDirectory[16](資料目錄表):
- (三)區塊表與區塊
- ——區塊合并
- ——區塊對齊與地址轉換運算
- 三、輸入表(.idata區塊)
- (一)輸入函式的呼叫
- (二)輸入表的結構 與 IAT/INT
- (三)實體分析
- 載入前的輸入表實體分析
- 載入后的輸入表實體分析
一、PE檔案的載入機制:
PE檔案并不是作為單一映射檔案被載入,它先被Windows加載器(PE裝載器)遍歷,決定哪個部分要被映射,(映射是高偏移地址對應高記憶體地址),
然后PE檔案被載入記憶體,資料結構布局與原始的一致之外,資料間的相對位置不一定一致(如下圖),所以某一部分的載入偏移地址不一定等于原始偏移地址,被載入到記憶體的部分統稱模塊,那么模塊句柄其實就是映射檔案(等同于 PE檔案 的虛擬空間 )的起始地址(但 Windows CE 除外),它還有另一個名字叫基地址,我們在Windows編程時用到的API函式GetModuleHandle就是用來獲取模塊句柄,即基地址 用的,
PE檔案的基地址由檔案自身決定,按照默認設定,VC++編譯鏈接生成的EXE檔案的基地址是400000h,DLL檔案則是10000000h,

(一)相對虛擬地址RVA:
相對虛擬地址是PE檔案在 記憶體中的相對于PE檔案載入地址(基地址)的偏移地址,它被采用的意義就在于,載入的指標可以在記憶體中的任意位置,可以很好地確定某一部分的具體位置,
相 對 虛 擬 地 址 = 目 標 地 址 ( 虛 擬 地 址 ) ? 載 入 地 址 ( 基 地 址 ) 相對虛擬地址 = 目標地址(虛擬地址) - 載入地址(基地址) 相對虛擬地址=目標地址(虛擬地址)?載入地址(基地址)
(二)檔案偏移地址(物理地址):
檔案偏移地址是PE檔案在磁盤中相對于檔案頭的偏移地址,
其實我們用winhex等16進制編輯器打開一個PE檔案后,在左邊欄呈現的就是檔案偏移地址,即物理地址,

二、PE檔案結構簡述
(一)MS-DOS頭部
由DOS MZ(MZ頭)和緊隨其后的DOS stub(DOS塊)組成
- MZ頭告訴DOS這是一個有效執行體【就把他當作檔案頭就好】
- DOS塊由編譯器自動生成
這個結構中有用的欄位主要是e_magic和e_lfanew
e_magic:MZ的ASCII編碼,可執行檔案都以其開頭,e_lfanew:記錄真正PE頭的RVA,

(二)PE檔案頭
Windows加載器會讀取e_lfanew欄位,進而算出PE檔案頭的指標,PE檔案頭又可以被劃分成3個部分,分別是Signature、IMAGE_FILE_HEADER(映像檔案頭)、IMAGE_OPTIONAL_HEADER(可選檔案頭)
Signature:"PE\0\0"的ASCII編碼映像檔案頭:記錄一些PE檔案的基本資訊,里面的一個欄位指出可選映像頭的大小,具體參考 《加密與解密(第四版)》P.409可選映像頭:更詳盡地描述了PE檔案的基本資訊,一些有用的資訊可以通過LordPE等PE編輯器直觀地看出來,在這里詳細說說位于可選映像頭的一個重要欄位DataDirectory[16],其他的具體參考 《加密與解密(第四版)》P.409
——DataDirectory[16](資料目錄表):
這16個元素的結構都是
IMAGE_DATA_DIRECTORY(記錄偏移地址和占用空間大小),指向輸入表、輸出表、資源塊等資料,用于定位輸入輸出表等資源,
打開LordPE,進入目錄界面可以直觀的看到資料目錄表的資訊,
(三)區塊表與區塊
區塊表:緊接著可選映像頭就是區塊表,是一個IMAGE_SECTION_HEADER結構陣列,一個IMAGE_SECTION_HEADER結構對應一個區塊,每個這樣的結構記錄的是對應區塊的基本資訊,而這個陣列有多少個取決于PE檔案頭的映像檔案頭的NumberOfSections欄位的數值,
我們用LordPE編輯一個PE檔案的時候,進入區段視窗可以清晰的看到各個區塊即它們的一些重要資訊
關于塊屬性欄位Characteristic,它的值是由下表中的值相或得到:
地址 用途 00000020h 包含代碼,常與10000000h一起設定 00000040h 該塊包含已經初始化的資料 00000080h 該塊包含未初始化的資料 02000000h 該塊可丟棄,一旦被載入,行程便不再需要它,例如重定位塊.reloc 10000000h 該塊為共享塊 20000000h 該塊可執行,通常00000020h被設定時它也被設定 40000000h 該塊可讀,可執行檔案中的塊總是設定該標志 80000000h 該塊可寫,如果PE檔案中沒有設定該標志,裝載程式就將記憶體映像頁標記為可讀或者可執行
區塊:一個PE檔案至少有兩個區塊(代碼塊和資料塊)組成,在映像中的排列順序按照起始地址排列而非字母表,不額外自定義區塊名的情況下,聯結器給這些區塊的命名是由聯結器本身決定的(微軟的聯結器和Borland的聯結器設定的名稱不同),常見區塊命名詳見《加密與解密(第四版)》P.417
——區塊合并
從源代碼到可執行檔案的程序中,一些區塊在OBJ檔案時就已經被放置了,可能還有特殊的用于給聯結器傳遞訊息的區塊,而
聯結器做的就是按照一定規則合并OBJ和區塊,這樣做可以節省磁盤與記憶體空間,
(要是合并的程序中要合并的區塊有一個是只讀屬性,那么系統臨時將其設定為可讀可寫,再進行合并操作,初始化之后恢復)——區塊對齊與地址轉換運算
PE檔案頭的可選映像頭中的FIleAlignment欄位定義了磁盤區塊的對齊值,SectionAligment欄位定義了記憶體區塊的對齊值,
- 磁盤中,每一個區塊以
磁盤區塊對齊值的整數倍作為偏移地址,不足的地方(區塊間隙)用00h填充,- 記憶體中,區塊至少從一個頁邊界處開始
當區塊在記憶體中的偏移跟檔案中的偏移一致時可以提高載入速度,但會使可執行檔案變大,這么做取決于檔案是否足夠小,
結合下面的磁盤到記憶體的映射圖我們發現,MS-DOS到塊表的部分無論是在磁盤中還是在記憶體中,它們的偏移都是一致的,不一致的是其后的塊的偏移,由于區塊對齊,對于不同塊來說,各個塊在記憶體中的偏移與磁盤中的偏移的差值是不一定相同,但在同一個區塊中磁盤與記憶體的對應地址,這個差值又是相同的,不難得出下面的公式:
F i l e O f f s e t = RVA ? Δ k \textcolor{red}{{\mathnormal{FileOffset}} = \text{RVA} -\varDelta{k}} FileOffset=RVA?Δk
F i l e O f f s e t = RVA ? I m a g e B a s e ? Δ k \textcolor{red}{\mathnormal{FileOffset} = \text{RVA} - {ImageBase} - \varDelta{k}} FileOffset=RVA?ImageBase?Δk
三、輸入表(.idata區塊)
(一)輸入函式的呼叫
PE檔案載入記憶體前,要用到的輸入函式的基本資訊已經存在于PE檔案中,但Windows加載器在PE檔案載入記憶體之后才將相關DLL載入記憶體,并將呼叫輸入函式的指令與輸入函式的實際地址關聯,同時輸入地址表(IAT)中也被寫入了輸入函式的地址,
由于使用來自其他DLL的代碼和資料的程序叫
輸入,所以輸入函式即外部函式
有的時候程式本可以直接用下面匯編陳述句高效呼叫API
call DWORD PTR [某API的地址]
但因為編譯器分辨不出輸入函式的呼叫和普通函式的呼叫而一視同仁使用下面的低效呼叫方式,
call 地址1 ;子程式
…… ……
地址1:
jmp dword ptr [某API的地址]
我們只需要在輸入函式的申明前面加上_declspec(dllimport)即可解決這個問題
(二)輸入表的結構 與 IAT/INT
IAT:輸入地址表INT:輸入名稱表
輸入表以一個IMAGE_IMPORT_DIRECTORY(IID)結構陣列開始,一個IID對應一個DLL等等,陣列的最后一個元素是一個內容全0的IID作為該陣列結束的標志,
IID中有兩個很重要的欄位OriginalFirstThunk和FirstThunk,分別指向輸入名稱表INT和輸入地址表IAT的虛擬偏移地址,
IAT和INT本質上是一個IMAGE_THUNK_DATA陣列,陣列中的一個元素即一個IMAGE_THUNK_DATA(雙字)對應一個輸入函式,而IMAGE_THUNK_DATA本質上是指標,在不同的時刻有不同含義
IMAGE_THUNK_DATA的最高位為1:函式以序號方式輸入,此時低31位代表被輸入API的序數值,IMAGE_THUNK_DATA的最高位為0:函式以字串型別的函式名方式輸入,此時雙字指向一個IMAGE_IMPORT_BY_NAME結構(單字)
IMAGE_IMPORT_BY_NAME結構存盤一個輸入函式的相關資訊如下:
hint:占一個字,輸入函式在外部DLL輸出表中的序號name:所占空間可變,輸入函式的函式名稱
在PE檔案加載到記憶體前,所有IMAGE_THUNK_DATA結構都指向IMAGE_IMPORT_BY_NAME結構,
IAT和INT都是以一個內容全為0的IMAGE_THUNK_DATA結構作為結束標志,

也就是說,在載入記憶體前,PE檔案的IAT和INT都是指向IMAGE_IMPORT_BY_NAME結構的,但在載入記憶體之后,Windows加載器通過INT找到所有輸入函式的地址,然后用這些地址去替代IAT中指向IMAGE_IMPORT_BY_NAME結構的地址,此時IAT存入了輸入函式的地址,輸入表中別的部分已經不再重要,

(三)實體分析
下面我們以一個PE檔案為例用
010 Editor和LordPE分別從載入記憶體前和載入記憶體后分析它的輸入表,
載入前的輸入表實體分析
我們知道,可選映像頭中資料目錄欄位的第二個元素記錄的就是輸入表的RVA,資料目錄表相對于PE頭的偏移是80h,在010 Editor中可以很容易定位得到輸入表的RVA的值為13A1EC

LordPE中也可以一目了然得到,

目錄表界面下輸入表旁點擊H按鈕即可立即查看輸入表的內容(黑色底紋處)

要想在010 Editor中定位輸入表的位置需要知道的是物理地址,但物理地址并不等于相對虛擬地址(RVA),所以就要用到物理地址跟虛擬地址的轉換,
可以直接用LordPE內置的檔案位置計算器就可以得到檔案偏移地址


提取出來如下,5個雙字為一個IID,
| OriginalFirstThunk | TimeDateStamp | Forward | Name(指向了DLL的名稱) | First Thunk |
|---|---|---|---|---|
| E4A3 1300 | 0000 0000 | 0000 0000 | 22A4 1300 | BCA1 1300 |
| 28A2 1300 | 0000 0000 | 0000 0000 | 4EAA 1300 | 00A0 1300 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 |
以第一個IID為例,Name是指向DLL名稱的指標,因為高位對應高地址,所以22A4 1300倒置過來就是
RVA
=
0013
A
422
\textcolor{red}{\text{RVA} = 0013A422}
RVA=0013A422,位置計算器計算出物理地址E0A22

010 Editor中定位,可以發現第一個DLL對應USER32.dll

之前探討過,加載到記憶體之前,IAT和INT結構相同,數值一致,OriginalFirstThunk指向INT,第一個IID的OriginalFirstThunk為0013A3E4,物理地址對應0E09E4,

定位到0E09E4,

再看看第一個IID中指向IAT的First Thunk欄位,
RVA
=
0013
A
1
B
C
\textcolor{red}{\text{RVA} = 0013A1BC}
RVA=0013A1BC,對應物理地址0E07BC

定位到0E07BC,發現IAT與INT此時的值一致,都是0013A414

最高位為0,說明以函式名的方式輸入,包含了函式名名稱的IMAGE_IMPORT_BY_NAME結構的RVA為0013A414,物理地址是000E0A14

得到物理地址后010 Editor中定位,可以發現這個IMAGE_IMPORT_BY_NAME結構對應的輸入函式是MessageBoxA函式

載入后的輸入表實體分析
我們把程式運行程序中記憶體中的資料dump下來,然后再分析輸入表,這里可以寫個程式dump,也可以直接用OD插件,我選擇的是用OD插件的方式
先用LordPE找到輸入表的RVA為0013A1EC,由于是dump出來的程式,此時的RVA已經是等于物理地址了,不需要再轉換,

010 Editor中定位,與載入記憶體前的輸入表一致,

現在看看第一個IID的OriginalFirstThunk欄位指向的INT,物理地址即RVA 為0013A3E4,資料相比載入記憶體前沒有改變

再分析一下FirstThunk指向的IAT,物理地址即RVA 為0013A1BC,資料產生了變化,變成了75A1 ED60,這應該就是USER32.dll鏈接庫中MessageBox函式的地址

轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/286446.html
標籤:區塊鏈
上一篇:go 語言開發環境如何搭建?
下一篇:Java基礎--多執行緒和分布式




