2.10 PE結構:重建重定位表結構

2023-09-09 15:00:11

Relocation(重定位)是一種將程式中的一些地址修正為執行時可用的實際地址的機制。在程式編譯過程中,由於程式中使用了各種全域性變數和函數,這些變數和函數的地址還沒有確定,因此它們的地址只能暫時使用一個相對地址。當程式被載入到記憶體中執行時,這些相對地址需要被修正為實際的絕對地址,這個過程就是重定位。

在Windows作業系統中,程式被載入到記憶體中執行時,需要將程式中的各種記憶體地址進行重定位,以使程式能夠正確地執行。Windows系統使用PE(Portable Executable)檔案格式來儲存可執行程式,其中包括重定位資訊。當程式被載入到記憶體中時,系統會解析這些重定位資訊,並將程式中的各種記憶體地址進行重定位。

重定位表一般出現在DLL中,因為DLL都是動態載入,所以地址不固定,DLL的入口點在整個執行過程中至少要執行2次,一次是在開始時執行初始化工作,一次則是在結束時做最後的收尾工作,重定位表則是解決DLL的地址問題,為了能找到重定位表首先我們需要使用PeView工具查詢DataDirectory資料目錄表,在其中找到Base relocation欄位,裡面的0x00001800則是重定位表基地址;

我們通過使用WinHex工具定位到0x00001800即可看到重定位表資訊,如下圖中的1000代表的是重定位RVA地址,綠色的0104代表的則是重定位塊的長度,後面則是每兩個位元組代表一個重定位塊,0A是重定位地址,30則是重定位的型別,以此順序向下排列。

重定位表也是分頁排列的,每一頁大小都是1000位元組,通過使用FixRelocPage命令即可查詢到當前程式中的重定位塊資訊,並以第一個為例,查詢一下起始地址RVA為1000的頁上,有哪些重定位結構,如下圖所示;

其中的重定位RVA地址0000100A是用標黃色的1000加上標藍色的0xA得到的。而修正RVA地址00003000加上模組基地址63FF0000+3000得到的則是第一個被修正的記憶體地址,讀者可使用x64dbg跳轉到該程式內自行確認。

重定位表的修復原理與IAT修復完全一致,我們需要分別讀入脫殼前與脫殼後的兩個程式,接著通過迴圈正確的重定位表資訊,並依次覆蓋到脫殼後的程式內,以此實現對重定位表的修復功能,實現程式碼如下所示;

#include <windows.h>
#include <stdio.h>

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

DWORD FileSize = 0;  // 定義檔案大小
DWORD FileBase = 0;  // 儲存檔案的基地址

// 定義全域性變數,來儲存DOS,NT,Section頭
PIMAGE_DOS_HEADER DosHeader = nullptr;
PIMAGE_NT_HEADERS NtHeader = nullptr;
PIMAGE_FILE_HEADER FileHead = nullptr;

// 將RVA轉換為FOA的函數
DWORD RVAtoFOA(DWORD rva)
{
  auto SectionTables = IMAGE_FIRST_SECTION(NtHeader);    // 獲取區段表
  WORD Count = NtHeader->FileHeader.NumberOfSections;    // 獲取區段數量

  for (int i = 0; i < Count; ++i)
  {
    // 判斷是否存在於區段中
    DWORD Section_Start = SectionTables[i].VirtualAddress;
    DWORD Section_Ends = SectionTables[i].VirtualAddress + SectionTables[i].SizeOfRawData;
    if (rva >= Section_Start && rva < Section_Ends)
    {
      // 找到之後計算位置並返回值
      return rva - SectionTables[i].VirtualAddress + SectionTables[i].PointerToRawData;
    }
  }
  return -1;
}

// 開啟PE檔案
bool OpenPeFile(LPCSTR FileName)
{
  // 開啟檔案
  HANDLE Handle = CreateFileA(FileName, GENERIC_READ, NULL,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (Handle == INVALID_HANDLE_VALUE)
    return false;

  // 獲取檔案大小
  FileSize = GetFileSize(Handle, NULL);

  // 讀取檔案資料
  DWORD OperSize = 0;
  FileBase = (DWORD)new BYTE[FileSize];
  ReadFile(Handle, (LPVOID)FileBase, FileSize, &OperSize, NULL);

  // 獲取DOS頭並判斷是不是一個有效的DOS檔案
  DosHeader = (PIMAGE_DOS_HEADER)FileBase;
  if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    return false;

  // 獲取 NT 頭並判斷是不是一個有效的PE檔案
  NtHeader = (PIMAGE_NT_HEADERS)(FileBase + DosHeader->e_lfanew);
  if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
    return false;

  // 判斷是不是一個32位元檔案
  if (NtHeader->OptionalHeader.Magic != 0x010B)
    return false;
  
  CloseHandle(Handle);
  return true;
}

// 修復重定位表
void RepairFixReloc(char new_file[])
{
  DWORD base = NtHeader->OptionalHeader.ImageBase;
  // 1. 獲取重定位表的 rva
  DWORD RelocRVA = NtHeader->OptionalHeader.DataDirectory[5].VirtualAddress;
  // 2. 獲取重定位表
  auto Reloc = (PIMAGE_BASE_RELOCATION)(FileBase + RVAtoFOA(RelocRVA));

  // 3. 遍歷重定位表中的重定位塊,以0結尾
  while (Reloc->SizeOfBlock != 0)
  {
    // 3.1 輸出分頁基址
    printf("[↓] 分頁基址: 0x%08X \n\n", Reloc->VirtualAddress);
    // 3.2 找到重定位項
    auto Offset = (TypeOffset*)(Reloc + 1);

    // 3.3 計算重定位項的個數
    // Reloc->SizeOfBlock 儲存的是整個重定位塊的大小 結構體 + 重定位項陣列
    // Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) 得到單個陣列大小
    // 上面的結果 \ 2 = 重定位項的個數,原因是重定位項的大小為兩個位元組
    DWORD Size = (Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

    // 3.4 遍歷所有的重定位項
    for (DWORD i = 0; i < Size; ++i)
    {
      DWORD Type = Offset[i].Type;                  // 獲取重定位型別,只關心為3的型別
      DWORD pianyi = Offset[i].Offset;              // 獲取重定位的偏移值
      DWORD rva = pianyi + Reloc->VirtualAddress;   // 獲取要重定位的地址所在的RVA
      DWORD foa = RVAtoFOA(rva);                    // 獲取要重定位的地址所在的FOA
      DWORD fa = foa + FileBase;                    // 獲取要重定位的地址所在的fa
      DWORD addr = *(DWORD*)fa;                     // 獲取要重定位的地址
      DWORD new_addr = addr - base + 0x1500000;     // 計算重定位後的資料: addr - oldbase + newbase

      // 將重定位後的資料寫回緩衝區(檔案)
      if (Offset[i].Type == 3)
        *(DWORD*)fa = new_addr;

      printf("\t [->] 重定位RVA: 0x%08X | 重定位FOA: 0x%08X | 重定位地址: 0x%08X | 修正地址: 0x%08X \n", rva, foa, addr, new_addr);
    }
    // 找到下一個重定位塊
    Reloc = (PIMAGE_BASE_RELOCATION)((DWORD)Reloc + Reloc->SizeOfBlock);
  }
  // 儲存修正後的檔案
  NtHeader->OptionalHeader.ImageBase = 0x1500000;

  // 開啟一個新檔案
  HANDLE new_handle = CreateFileA(new_file, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (new_handle == INVALID_HANDLE_VALUE)
    return;

  DWORD OperSize = 0;
  // 儲存修正好的程式
  BOOL ret = WriteFile(new_handle, (LPVOID)FileBase, FileSize, &OperSize, NULL);
  if (ret == TRUE)
  {
    printf("\n\n");
    CloseHandle(new_handle);

    printf("[*] 修復 %s 檔案 \t 寫入基址: %08X \t 總長度: %d \t 寫入長度: %d \n", new_file, FileBase, FileSize, OperSize);
  }
}

void Banner()
{
  printf(" ____        _ _     _   ____      _            \n");
  printf("| __ ) _   _(_) | __| | |  _ \\ ___| | ___   ___ \n");
  printf("|  _ \\| | | | | |/ _` | | |_) / _ \\ |/ _ \\ / __|\n");
  printf("| |_) | |_| | | | (_| | |  _ <  __/ | (_) | (__ \n");
  printf("|____/ \\__,_|_|_|\\__,_| |_| \\_\\___|_|\\___/ \\___|\n");
  printf("                                                \n");
  printf("Reloc 重定位錶快速修復工具 \t By: LyShark \n");
  printf("Usage: BuildFix [原檔案位置] [修復後檔案位置] \n\n\n");
}

int main(int argc, char* argv[])
{
  Banner();
  if (argc == 3)
  {
    bool flag = OpenPeFile(argv[1]);
    if (true == flag)
    {
      RepairFixReloc(argv[2]);
    }
  }
  return 0;
}

執行上述程式,讀者可自行傳入脫殼前的程式與脫殼後的程式,此時則會實現自動替換,如下圖所示;

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