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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...