2.7 PE結構:重定位表詳細解析

2023-09-07 13:28:30

重定位表(Relocation Table)是Windows PE可執行檔案中的一部分,主要記錄了與地址相關的資訊,它在程式載入和執行時被用來修改程式程式碼中的地址的值,因為程式在不同的記憶體地址中載入時,程式中使用到的地址也會受到影響,因此需要重定位表這個資料結構來完成這些地址值的修正。

當程式需要被載入到不同的記憶體地址時,相關的地址值需要進行修正,否則程式執行會出現異常。而重定位表就是記錄了在程式載入時需要修正的地址值的相關資訊,包括修正地址的位置、需要修正的位元組數、需要修正的地址的型別等。重定位表中的每個記錄都稱為一項(entry),每個entry包含了需要修正的地址值的詳細資訊,通常是以可變長度資料的形式儲存在一個或多個叫做重定位塊(relocation block)的資料結構中。

解析重定位表需要通過PIMAGE_BASE_RELOCATION這個關鍵結構體來實現,PIMAGE_BASE_RELOCATION是一個指向重定位表(Relocation Table)的指標型別,它是Windows PE可執行檔案中用於支援動態基地址重定位(Dynamic Base Relocation)的結構體型別。在2GB以上的虛擬地址下,Windows使用了Dynamic Base Relocation技術來提高系統的安全性,PIMAGE_BASE_RELOCATION就是在這種情況下使用的。

由於Windows系統中DLL檔案並不能每次都能載入到預設的基址上,因此基址重定位主要應用於DLL檔案中,通常涉及到直接定址的指令就需要重定位,重定位資訊是在編譯時,由編譯器生成並被儲存在可執行檔案中的,在程式被執行前,由作業系統根據重定位資訊修正程式碼,這樣在開發程式的時候就不用了考慮重定位問題了,我們還是使用上面的這段組合程式碼。

00D21000 | 6A 00              | push 0x0                            |
00D21002 | 68 0030D200        | push main.D23000                    |  
00D21007 | 68 0730D200        | push main.D23007                    |  
00D2100C | 6A 00              | push 0x0                            |
00D2100E | E8 07000000        | call <JMP.0x00D2101A>               | call MessageBox
00D21013 | 6A 00              | push 0x0                            |
00D21015 | E8 06000000        | call <JMP.0x00D21020>               | call ExitProcess
00801017 | CC                 | int3                                |
00D2101A | FF25 0820D200      | jmp dword ptr ds:[<&0x00D22008>]    | 匯入函數地址
00D21020 | FF25 0020D200      | jmp dword ptr ds:[<&0x00D22000>]    | 匯入函數地址

如上jmp dword ptr ds:[<&0x00D22008>]這段程式碼就是一句需要重定位的程式碼,當程式的基地址位於0x00D20000時,這段程式碼中的函數可以被正常呼叫,但有時程式會開啟基址隨機化,或DLL被動態裝載等問題,此時基地址可能會發生變化,那麼上面的組合指令呼叫就會失效,這就意味著這些地址需要被修正。

此時我們假設程式基址變為了0x400000,那麼jmp dword ptr ds:[<&0x00D22008>]這條指令就需要被修正,修正演演算法可以描述為,將直接定址指令中的地址加上模組實際裝入地址與模組建議裝入地址之差,為了進行運算需要3個資料,首先是需要修正機器碼地址,其次是模組建議裝入地址,最後是模組的實際裝入地址。

在這3個資料中,模組的建議裝入地址已經在PE檔案頭中定義了,而模組的實際裝入地址時Windows裝載器在裝載檔案時確定的,事實上PE檔案重定位表中儲存的僅僅只是,一大堆需要修正的程式碼的地址。

重定位表IMAGE_BASE_RELOCATION解析

重定位表會被單獨存放在.reloc命名的節中,重定位表的位置和大小可以從資料目錄中的第6個IMAGE_DATA_DIRECTORY結構中獲取到,該表的組織方式時以0x1000頁為一塊,每一塊負責一頁,從PE檔案頭獲取到重定位表地址後,就可以順序讀取到所有表結構,每個重定位塊以一個IMAGE_BASE_RELOCATION結構開頭,後面跟著在本頁中使用的所有重定位項,每個重定位項佔用16位元組,最後一個節點是一個使用0填充的_IMAGE_BASE_RELOCATION標誌表的結束,其結構如下所示:

typedef struct _IMAGE_BASE_RELOCATION
{   
    DWORD   VirtualAddress;                      // 需重定位資料的起始RVA   
    DWORD   SizeOfBlock;                         // 本結構與TypeOffset總大小 
    WORD    TypeOffset[1];                       // 原則上不屬於本結構 
} IMAGE_BASE_RELOCATION; typedef  IMAGE_BASE_RELOCATION UNALIGNED IMAGE_BASE_RELOCATION;

TypeOffset的元素個數 = (SizeOfBlock - 8 )/ 2 TypeOffset的每個元素都是一個自定義型別結構

struct
{
    WORD Offset:12;  // 大小為12Bit的重定位偏移 
    WORD Type  :4;   // 大小為4Bit的重定位資訊型別值 
}TypeOffset;         // 這個結構體是A1Pass總結的

PIMAGE_BASE_RELOCATION指標指向PE檔案中的重定位表(Relocation Table)的起始地址,重定位表是一個可變長度的資料結構,其中包含了一組以4個位元組為單位的記錄,每個記錄表示一個需要修正的地址及其操作型別。

typedef struct _IMAGE_BASE_RELOCATION
{
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

每個重定位表項(Relocation Table Entry)包括兩部分:前16位元表示需要修正的地址的偏移量(Offset),後16位元則表示需要對該地址進行什麼樣的修正操作(Relocation Type)。普通的重定位項型別有如下幾種:

  • IMAGE_REL_BASED_ABSOLUTE:表示不需要進行任何修正;
  • IMAGE_REL_BASED_HIGHLOW:表示需要將地址中的低16位元和高16位元分別進行修正;
  • IMAGE_REL_BASED_DIR64:表示需要對64位元指標進行修正;

當讀者需要遍歷這個表時,首先可以通過NtHeader->OptionalHeader.DataDirectory[5].VirtualAddress獲取到重定位表的相對資訊,並通過(PIMAGE_BASE_RELOCATION)(GlobalFileBase + RVAtoFOA(RelocRVA))得到重定位表的FOA檔案地址,在Reloc->SizeOfBlock變數內獲取到重定位塊,並回圈輸出則可實現列舉所有重定位塊;

// --------------------------------------------------
// 重定位表解析結構體
// --------------------------------------------------
struct TypeOffset
{
    WORD Offset : 12;       // 低12位元代表重定位地址
    WORD Type : 4;          // 高4位元代表重定位型別
};

int main(int argc, char * argv[])
{
    BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe"), 0);

    if (PE == TRUE)
    {
        // 1.拿到映像基地址
        DWORD base = NtHeader->OptionalHeader.ImageBase;

        // 2.獲取重定位表的RVA 相對偏移
        DWORD RelocRVA = NtHeader->OptionalHeader.DataDirectory[5].VirtualAddress;

        // 3.獲取重定位表FOA
        auto Reloc = (PIMAGE_BASE_RELOCATION)(GlobalFileBase + RVAtoFOA(RelocRVA));

        printf("映像基址: %08X 虛擬偏移: %08X 重定位表基址: %08X \n", base, RelocRVA, Reloc);

        // 4.遍歷重定位表中的重定位塊,以0結尾
        while (Reloc->SizeOfBlock != 0)
        {
            // 計算出重定位項個數 \ 2 = 重定位項的個數,原因是重定位項的大小為2位元組
            DWORD Size = (Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

            // 輸出VirtualAddress分頁基址 與SizeOfBlock重定位塊長度
            printf("起始RVA: %08X \t 塊長度: %04d \t 重定位個數: %04d \n", Reloc->VirtualAddress, Reloc->SizeOfBlock, Size);

            // 找到下一個重定位塊
            Reloc = (PIMAGE_BASE_RELOCATION)((DWORD)Reloc + Reloc->SizeOfBlock);
        }
    }
    else
    {
        printf("非標準程式 \n");
    }

    system("pause");
    return 0;
}

編譯並執行上述程式碼片段,則讀者可以看到當前程式內所具備的重定位塊及該塊的記憶體地址,輸出效果圖如下所示;

上圖中我們得到了0x905a4d00這個記憶體地址,該記憶體地址代表的則是重定位表中一個塊的基址,如果我們需要得到該基址內的其他重定位資訊,則需要進一步遍歷,這個遍歷過程只需要更加細化將如上程式碼片段進行更改,增加更加細緻的列舉過程即可,更改後的程式碼片段如下所示;

// --------------------------------------------------
// 傳入一個十六進位制字串,將其自動轉化為十進位制格式:例如傳入40158b轉為4199819
// --------------------------------------------------
int HexStringToDec(char hexStr[])
{
    int i, m, n, temp = 0;

    // 迴圈讀入每一個十六進位制數
    m = strlen(hexStr);
    for (i = 0; i < m; i++)
    {
        // 十六進位制還要判斷他是不是在A-F或0-9之間的數
        if (hexStr[i] >= 'A' && hexStr[i] <= 'F')
            n = hexStr[i] - 'A' + 10;
        else if (hexStr[i] >= 'a' && hexStr[i] <= 'f')
            n = hexStr[i] - 'a' + 10;
        else n = hexStr[i] - '0';
        // 將資料加起來
        temp = temp * 16 + n;
    }
    return temp;
}

int main(int argc, char * argv[])
{
    BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe"), 0);
    char * GetRva = "0x905a4d00";

    if (PE == TRUE)
    {
        DWORD base = NtHeader->OptionalHeader.ImageBase;

        // 1. 獲取重定位表的 rva
        DWORD RelocRVA = NtHeader->OptionalHeader.DataDirectory[5].VirtualAddress;

        // 2. 獲取重定位表
        auto Reloc = (PIMAGE_BASE_RELOCATION)(GlobalFileBase + RVAtoFOA(RelocRVA));

        printf("起始RVA \t 型別 \t 重定位RVA \t 重定位地址 \t 修正RVA \n");

        // 起始RVA:% 08X-- > 型別:% d-- > 重定位RVA:% 08X-- > 重定位地址:% 08X 修正RVA : % 08X

        // 3. 遍歷重定位表中的重定位塊,以0結尾
        while (Reloc->SizeOfBlock != 0)
        {
            // 3.2 找到重定位項
            auto Offset = (TypeOffset*)(Reloc + 1);

            // 3.3 計算重定位項的個數
            DWORD Size = (Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

            // 3.4 遍歷所有的重定位項
            for (DWORD i = 0; i < Size; ++i)
            {
                // 獲取重定位型別,只關心為3的型別
                DWORD Type = Offset[i].Type;

                // 獲取重定位的偏移值
                DWORD pianyi = Offset[i].Offset;

                // 獲取要重定位的地址所在的RVA: offset+virtualaddress
                DWORD rva = pianyi + Reloc->VirtualAddress;

                // 獲取要重定位的地址所在的FOA
                DWORD foa = RVAtoFOA(rva);

                // 獲取要重定位的地址所在的fa
                DWORD fa = foa + GlobalFileBase;

                // 獲取要重定位的地址
                DWORD addr = *(DWORD*)fa;

                // 計算重定位後的資料: addr - oldbase + newbase
                DWORD new_addr = addr - base;

                // 如果傳入了數值,則說明要遍歷特定的重定位表項
                if (Reloc->VirtualAddress == HexStringToDec(GetRva))
                {
                    printf("%08X \t %d \t %08X \t %08X \t%08X \n", Reloc->VirtualAddress, Type, rva, addr, new_addr);
                }
                // 否則如果不傳引數,則預設遍歷全部RVA
                else if (strcmp(GetRva, "all") == 0)
                {
                    printf("%08X \t %d \t %08X \t %08X \t%08X \n", Reloc->VirtualAddress, Type, rva, addr, new_addr);
                }
            }
            // 找到下一個重定位塊
            Reloc = (PIMAGE_BASE_RELOCATION)((DWORD)Reloc + Reloc->SizeOfBlock);
        }
    }
    else
    {
        printf("非標準程式 \n");
    }

    system("pause");
    return 0;
}

當讀者執行這段程式,則會輸出0x905a4d00這段記憶體地址中所具有的所有重定位資訊,輸出效果圖如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/72fc3188.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!