1. 程式人生 > >PE總結 – 重定位表

PE總結 – 重定位表

一、什麼是重定位?

重定位就是你本來這個程式理論上要佔據這個地址,但是由於某種原因,這個地址現在不能讓你霸佔,你必須轉移到別的地址,這就需要基址重定位。

二、為什麼需要重定位?

這個和上面的問題的解釋是一樣的。不是說過每個程序都有自己獨立的虛擬地址空間嗎?既然都是自己的,怎麼會被佔據呢?對於EXE應用程式來說,是這樣的。但是動態連結庫就不一樣了,我們說過動態連結庫都是寄居在別的應用程式的空間的,所以出現要載入的基地址被應用程式佔據了也是很正常的,這時它就不得不進行重定位了。

三、重定位表的結構

1、重定位所需的資料

在開始分析重定位表的結構之前需要了解兩個問題:第一,對一條指令進行重定位需要哪些資訊?第二,這些資訊中哪些應該被儲存在重定位表中?

下面舉例來說明這兩個問題,請看下面的這段程式碼:

:00400FFC 0000 ;dwVar變數
:00401000 55    push ebp
:00401001 8BEC    mov ebp, esp
:0040100383C4FC    add esp, FFFFFFFC
:00401006 A1FC0F4000   mov eax, dword ptr [00400FFC] ;mov eax,dwVar
:0040100B 8B45FC    mov eax, dword ptr [ebp-04] ;mov eax,@dwLocal
:0040100E 8B4508    mov eax, dword ptr [ebp+08] ;mov eax,_dwParam
:00401011 C9    leave
:00401012 C20400    ret 0004
:00401015 68D2040000   push 000004D2
:0040101A E8E1FFFFFF   call 00401000 ;invoke Proc1,1234

其中地址為00401006h處的mov eax,dword ptr [00400ffc]就是一句需要重定位的指令,當整個程式的起始地址位於00400000h處的時候,這句程式碼是正確的,假如將它移到00500000h處的時候,這句指令必須變成mov eax,dword ptr [00500ffc]才是正確的。這就意味著它需要重定位。

讓我們看看需要改變的是什麼,重定位前的指令機器碼是A1 FC0F 40 00,而重定位後將是A1 FC0F 50 00,也就是說00401007h開始的雙字00400ffch變成了00500ffch,改變的正是起始地址的差值(00500000h-00400000h)=00100000h。

所以,重定位的演算法可以描述為:將直接定址指令中的雙字地址加上模組實際裝入地址與模組建議裝入地址之差。為了進行這個運算,需要有3個數據,

首先是需要修正的機器碼地址;

其次是模組的建議裝入地址;

最後是模組的實際裝入地址。這就是第一個問題的答案。

在這3個數據中,模組的建議裝入地址已經在PE檔案頭中定義了,而模組的實際裝入地址是Windows裝載器確定的,到裝載檔案的時候自然會知道,所以第二個問題的答案很簡單,那就是應該被儲存在重定位表中的僅僅是需要修正的程式碼的地址。

事實上正是如此,PE檔案的重定位表中儲存的就是一大堆需要修正的程式碼的地址。

2、重定位表的位置

重定位表一般會被單獨存放在一個可丟棄的以“.reloc”命名的節中,但是和資源一樣,這並不是必然的,因為重定位表放在其他節中也是合法的,惟一可以肯定的是,如果重定位表存在的話,它的地址肯定可以在PE檔案頭中的資料目錄中找到。

3、重定位表的結構

雖然重定位表中的有用資料是那些需要重定位機器碼的地址指標,但為了節省空間,PE檔案對存放的方式做了一些優化。

在正常的情況下,每個32位的指標佔用4個位元組,如果有n個重定位項,那麼重定位表的總大小是4×n位元組大小。

直接定址指令在程式中還是比較多的,在比較靠近的重定位表項中,32位指標的高位地址總是相同的,如果把這些相近表項的高位地址統一表示,那麼就可以省略一部分的空間,當按照一個記憶體頁來分割時,在一個頁面中定址需要的指標位數是12位(一頁等於4096位元組,等於2的12次方),假如將這12位湊齊16位放入一個字型別的資料中,並用一個附加的雙字來表示頁的起始指標,另一個雙字來表示本頁中重定位項數的話,那麼佔用的總空間會是4+4+2×n位元組大小,計算一下就可以發現,當某個記憶體頁中的重定位項多於4項的時候,後一種方法的佔用空間就會比前面的方法要小。
PE檔案中重定位表的組織方法就是採用類似的按頁分割的方法,從PE檔案頭的資料目錄中得到重定位表的地址後,這個地址指向的就是順序排列在一起的很多重定位塊,每一塊用來描述一個記憶體頁中的所有重定位項。

每個重定位塊以一個IMAGE_BASE_RELOCATION結構開頭,後面跟著在本頁面中使用的所有重定位項,每個重定位項佔用16位的地址(也就是一個word),結構的定義是這樣的:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位記憶體頁的起始RVA
SizeOfBlock dd ? ;重定位塊的長度
IMAGE_BASE_RELOCATION ENDS
VirtualAddress欄位是當前頁面起始地址的RVA值,本塊中所有重定位項中的12位地址加上這個起始地址後就得到了真正的RVA值。SizeOfBlock欄位定義的是當前重定位塊的大小,從這個欄位的值可以算出塊中重定位項的數量,由於SizeOfBlock=4+4+2×n,也就是sizeof IMAGE_BASE_RELOCATION+2×n,所以重定位項的數量n就等於(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
IMAGE_BASE_RELOCATION結構後面跟著的n個字就是重定位項,每個重定位項的16位資料位中的低12位就是需要重定位的資料在頁面中的地址,剩下的高4位也沒有被浪費,它們被用來描述當前重定位項的種類。


雖然高4位定義了多種重定位項的屬性,但實際上在PE檔案中只能看到0和3這兩種情況。

所有的重定位塊最終以一個VirtualAddress欄位為0的IMAGE_BASE_RELOCATION結構作為結束,讀者現在一定明白了為什麼可執行檔案的程式碼總是從裝入地址的1000h處開始定義的了(比如裝入00400000h處的.exe檔案的程式碼總是從00401000h開始,而裝入10000000h處的.dll檔案的程式碼總是從10001000h處開始),要是程式碼從裝入地址處開始定義,那麼第一頁程式碼的重定位塊的VirtualAddress欄位就會是0,這就和重定位塊的結束方式衝突了。

四、例項分析

我們來分析一個例項,簡單分析一個DLL的重定位表,先反彙編:

根據上面的理論講解,我們需要重定位的有兩處:00402000和00403030

下面我們就來例項分析一下,是不是?首先找到重定位表的指標:


如果還不知道重定位表的RVA是怎麼找的,前參照我前面加的內容。

從圖中可以看出資料目錄表指向重定位表的指標是5000h,換算成檔案偏移地址就是0E00h,(也不要問我怎麼來的,我前面已經說明三個步驟)我們在定位到File Offset為0E00處,可以得到IMAGE_BASE_RELOCATION結構如下圖所示:


從圖中可以看出:
VirtualAddress:00001000h
SizeOfBlock:00000010h(有四個重定位資料,(10h-8h)/2h=4h)
重定位資料1:300Fh
重定位資料2:3023h
重定位資料3:0000h(用於對齊)
重定位資料4:0000h(用於對齊)
重定位資料計算過程如下表所示:


用十六進位制工具檢視例項檔案,其中060Fh和623h分別指向402000h和403030h,如下圖所示:


和我們上面假設的完全一樣!

執行PE檔案前,載入程式在進行重定位的時候,會將PE檔案在記憶體中的實際映像地址減去PE檔案所要求的映像地址,得到一個差值,再將這一差值根據重定位型別的不同新增到地址陣列中。