羽夏殼世界—— PE 結構(下)
寫在前面
此係列是本人一個字一個字碼出來的,包括程式碼實現和效果截圖。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏殼世界——序 ,方便學習本教程。
概述
匯入表和重定位表是一個比較複雜的結構,匯入表相對複雜,當然還有更復雜的資源表,不過這裡並不會介紹它。
匯入表是用來做什麼的?比如你在寫程式碼的時候,總會呼叫一些WinAPI
,而這些函式都是通過DLL
匯出的。匯入表的作用就是告訴作業系統載入該PE
DLL
,使用了哪些DLL
所提供的函式。重定位表可能稍微難理解一些,不過如果學習上篇應該就不太難了。我們在介紹
ImageBase
這個成員的時候,曾說它是PE
檔案傾向於要載入的地址,但是如果開了基址隨機或者是DLL
的話通常不會使用該地址,而這個成員就是用來進行重定位的,我們可以看看為什麼需要重定位:
如果執行上述程式碼,這個push
是所謂的死地址,也就是要puts
的字串,如果載入的基址並不是我們所謂的ImageBase
,而這個是作為硬編碼的一部分的,如果不改變的話,這地址是錯誤的,如果不進行重定位,就會導致列印的字串不對甚至報0xC0000005
錯誤,這個就是重定位的意義,我們來看示意圖:
下面我們來介紹匯入表和重定位表的結構:
匯入表
在介紹匯入表之前,我們先放個示意圖:
匯入表相關資訊是放到IMAGE_IMPORT_DESCRIPTOR
結構體中的,它的結構如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME; DWORD TimeDateStamp; //時間戳 DWORD ForwarderChain; //不使用 DWORD Name; //指向Ascii字串 DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
這結構體一個挨著一個,以空結構體為結尾(內容全為0的IMAGE_IMPORT_DESCRIPTOR
)。用一句話概括就是一個IMAGE_IMPORT_DESCRIPTOR
不定長陣列,通過最後一個是空表示結束。
OriginalFirstThunk
包含指向輸入名稱表INT
的RVA
。INT
是一個IMAGE_THUNK_DATA
結構的陣列,陣列中的每個IMAGE_THUNK_DATA
結構都指向IMAGE_IMPORT_BY_NAME
結構,陣列以一個內容為0的IMAGE_THUNK_DATA
結構結束。
TimeDateStamp
是一個32位的時間標誌,可以忽略。
ForwarderChain
是第1個被轉向的API
的索引,一般為0,在程式引用一個DLL
中的API
,而這個API
又在引用其他DLL
的API
時使用,但這樣的情況很少出現。
Name
是DLL名字的指標。它是一個以\0
結尾的ASCII
字元的RVA
地址,該字串包含輸入的DLL
名,例如KERNEL32.DLL
。
FirstThunk
包含指向輸入地址表IAT
的RVA
。IAT
是一個IMAGE_THUNK_DATA
結構的陣列。
OriginalFirstThunk
和FirstThunk
指向IMAGE_THUNK_DATA
陣列結構,結構是相似的,它的結構體如下所示:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;
上面的結構體32位和64位的區別不大,就是成員大小的問題。它是一個共用體的結構體,這個比較複雜,在不同的時刻具有不同的含義。
當IMAGE_THUNK_DATA
值的最高位為1
時,表示函式以序號方式輸人,這時低31
位,或者一個64位可執行檔案的低63
位,被看成一個函式序號。當雙字的最高位為0
時,表示函式以字串型別的函式名方式輸入,這時的值是一個RVA
,指向一個IMACE_IMPORT_BY_NAME
結構。
下面我們繼續介紹IMAGE_IMPORT_BY_NAME
,它的結構如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint
是本函式在其所駐留DLL
的輸出表中的序號。該域被PE
裝載器用來在DLL
的輸出表裡快速查詢函式。該值不是必需的,一些連結器將它設為·。
Name
含有輸入函式的函式名。函式名是一個ASCII
字串,以'\0'結尾。注意,這裡雖然將Name
的大小以位元組為單位進行定義,但其實它是一個可變尺寸域,是不定長的。
為了更好的理解,我們來看一個OriginalFirstThunk
指向的結構示意,FirstThunk
也是一樣的。
那麼IAT
與INT
到底有啥區別,我們來看個圖:
PE載入前
PE載入後
可以看出,當PE
檔案在磁碟的時候,這兩個儲存的東西是一模一樣的,但是被載入後,IAT
變為了地址表,而INT
被廢棄掉,不再使用。
由於匯入表的結構十分複雜,可能你看第一遍該博文的時候可能會犯糊塗,建議使用自己熟悉的程式語言手動解析PE
的匯入表,當你能夠比較輕鬆的解析它的時候,你就會明白匯入表的設計。
下面我們來看一下在二進位制檔案中相應的內容:
重定位表
重定位表的結構相對比較簡單,在學習之前請看下面的示意圖:
匯入表也是一個不定長陣列,用與匯入表相同的方式表示結束位置,每一個成員開頭描述都是一個IMAGE_BASE_RELOCATION
結構:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress
是指這組重定位資料的開始RVA
地址。各重定位項的地址加這個值才是該重定位項的完整RVA
地址。
SizeOFBlock
是當前重定位結構的大小。因為VirtualAddress
和SizeOfBlock
的大小都是固定的4位元組,所以這個值減8就是TypeOffset
陣列的大小。
TypeOffset
是一個數組。陣列每項大小為2位元組,共16
位。這16
位分為高4
位和低12
位。高4位代表重定位型別,低12位是重定位地址,它與VirtualAddress
相加就是指向PE映像中需要修改的地址資料的指標。
對於常見的重定位型別,如下所示:
型別 | 含義 |
---|---|
IMAGE_REL_BASED_ABSOLUTE | 沒有具體含義,只是為了讓每個段4位元組對齊 |
IMACE_REL_BASED_HIGHLOW | 重定位指向的整個地址都需要修正,實際上大部分情況下都是這樣的 |
IMAGE_REL_BASED_DIR64 | 出現在64位 PE 檔案中,對指向的整個地址進行修正 |
基址重定位資料採用類似按頁分割的方法組織,是由許多重定位塊串接成的,每個塊中存放4KB
的重定位資訊,每個重定位資料塊的大小必須以4位元組對齊。
下面我們來看一下一個64位程式的重定位表:
在二進位制檔案的位置和內容:
地址轉化
如果想要通過VA
得到RVA
,這個十分簡單:記憶體地址 – ImageBase
。
如果我們向通過RVA
得到FOA
,那麼怎麼樣呢?首先我們得判斷RVA
是否位於PE頭中,如果是FOA == RVA
,因為此時並沒有進行記憶體展開。如果RVA
不在的話,我們就得判斷RVA
位於哪個節。若RVA >= 節.VirtualAddress && RVA <= 節.VirtualAddress +當前節記憶體對齊後的大小
,那麼差值 = RVA - 節.VirtualAddress
,再加上相應的該節在檔案中的FOA
,就可以得到了真正的FOA
。
如何通過FOA
得到RVA
呢?這裡我就不多說了,原理是一樣的,只是判斷的東西不太一樣,正確答案將會在實現篇進行揭曉。
下一篇
羽夏殼世界——基礎篇小結