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

来源:https://www.cnblogs.com/LyShark/archive/2023/09/07/17684114.html
-Advertisement-
Play Games

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


重定位表(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 許可協議。轉載請註明出處!

文章作者:lyshark (王瑞)
文章出處:https://www.cnblogs.com/LyShark/p/17684114.html
本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # 集合總結 ## 一、概述 1. 作用:存儲對象的容器,代替數組的,使用更加的便捷 2. 所處的位置:java.util 3. 體繫結構 ![image](https://img2023.cnblogs.com/blog/3245131/202309/3245131-202309071934421 ...
  • Vue 3 的Composition API + ``` ``` 這就把清單功能獨立出來,可在任意需要的地方復用。 基於組件去搭建應用,可實現對業務邏輯的復用。如有其他頁面也需要用到這功能,直接復用。 然後,就可基於新語法實現清單應用。 把之前的代碼移植過來後,使用ref包裹的響應式數據。修改tit ...
  • # Python名稱空間和作用域,閉包函數 - 名稱的查詢順序 - 名稱空間的作用域 - global和nonlocal關鍵字的使用 - 函數對象(函數名) - 函數的嵌套調用 - 函數的嵌套定義 - 閉包函數 ## 名稱空間 ### 定義 ```python # 什麼是名稱空間? 名稱空間即存放名 ...
  • 作者:七寸知架構 \ 鏈接:https://www.jianshu.com/p/ec40a82cae28 # 1 引言# 本文主要講解JDBC怎麼演變到Mybatis的漸變過程,**重點講解了為什麼要將JDBC封裝成Mybaits這樣一個持久層框架**。再而論述Mybatis作為一個數據持久層框架本 ...
  • 網上查到的設計模式有23種,通過歸納去認識他們也是一種不錯的視角。 我這邊不按照主流的觀點去劃分為創建型、結構型、行為型三大類,我只歸納為創建型(Creational Class)、簡單功能場景(Simple Method Class)、複雜功能場景(Complex Method Class)三大類 ...
  • Matplotlib 中的圖例是幫助觀察者理解圖像數據的重要工具。圖例通常包含在圖像中,用於解釋不同的顏色、形狀、標簽和其他元素。 # 1. 主要參數 當不設置圖例的參數時,預設的圖例是這樣的。 ```python import numpy as np import matplotlib.pyplo ...
  • > 關註微信公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩,復旦機器人智能實驗室成員,阿裡雲認證的資深架構師,項目管理專業人士,上億營收AI產品研發負責人。 > 本文深入探討了 Go 語言的內 ...
  • 我們來看看如何通過幾個步驟快速的實現一個功能相對齊全的CLI程式。和做飯一樣,能夠快速獲得成就感的方式是找半成品直接下鍋炒一盤 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...