2.9 PE結構:重建匯入表結構

2023-09-08 12:02:43

脫殼修復是指在進行加殼保護後的二進位制程式脫殼操作後,由於加殼操作的不同,有些程式的匯入表可能會受到影響,導致脫殼後程式無法正常執行。因此,需要進行修復操作,將脫殼前的匯入表覆蓋到脫殼後的程式中,以使程式恢復正常執行。一般情況下,匯入表被分為IAT(Import Address Table,匯入地址表)和INT(Import Name Table,匯入名稱表)兩個部分,其中IAT儲存著匯入函數的地址,而INT儲存著匯入函數的名稱。在脫殼修復中,一般是通過將脫殼前和脫殼後的輸入表進行對比,找出IAT和INT表中不一致的地方,然後將脫殼前的輸入表覆蓋到脫殼後的程式中,以完成修復操作。

資料目錄表的第二個成員指向匯入表,該指標在PE開頭位置向下偏移0x80h處,此處PE開始位置為0xF0h也就是說匯入表偏移地址應該在0xf0+0x80h=170h如下圖中,匯入表相對偏移為0x21d4h

這個地址的讀取同樣可以使用PeView工具得到,通過輸入DataDirectory讀者可看到如下圖所示的輸出資訊,其中第二行則是匯入表的地址。

這裡的0x21d4是一個RVA地址,需要將其轉換為磁碟檔案FOA偏移才能定位到匯入表在檔案中的位置,使用RvaToFoa命令可快速完成計算,轉換後的檔案偏移為0x11d4

此處我們也可以通過使用虛擬偏移地址減去實際偏移地址來得到這個引數,由於0x21d4位於.rdata節,此時的rdata虛擬偏移是0x2000而實際偏移則是0x1000通過使用2000h-1000h=1000h,接著再通過0x21d4h-0x1000h=11D4h同樣可以得到相對FOA檔案偏移。

我們通過使用WinHex工具跳轉到11d4位置處,讀者此時能看到如下圖所示的地址資訊。

如上圖就是匯入表中的IID陣列,每個IID結構包含一個裝入DLL的描述資訊,現在有三個匯入DLL檔案,則第四個是一個全部填充為0的結構,標誌著IID陣列的結束,每一個結構有五個四位元組構成,該結構體定義如下所示;

typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
    union
    {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

我們以第一個呼叫動態連結庫為例,其地址與結構的說明如下所示:

  • 0000 22C0 => OrignalFirstThunk => 指向輸入名稱表INT的RVA
  • 0000 0000 => TimeDateStamp => 指向一個32位元時間戳,預設此處為0
  • 0000 0000 => ForwardChain => 轉向API索引,預設為0
  • 0000 244A => Name => 指向DLL名字的指標
  • 0000 209C => FirstThunk => 指向輸入地址表IAT的RVA

每個IID結構的第四個欄位指向的是DLL名稱的地址,以第一個動態連結庫為例,其RVA是0000 244A 將其減去1000h得到檔案偏移144A,跳轉過去看看,呼叫的是USER32.dll庫。

上方提到的兩個欄位OrignalFirstThunkFirstThunk都可以指向匯入結構,在實際裝入中,當程式中的OrignalFirstThunk值為0時,則就要看FirstThunk裡面的資料,FirstThunk常被叫做IAT它是在程式初始化時被動態填充的,而OrignalFirstThunk常被叫做INT,它是不可改變的,之所以會保留兩份是因為,有些時候會存在反查的需求,保留兩份是為了更方便的實現。

在上述流程中,我們找到了User32.dllOrignalFirstThunk,其地址為22C0,使用該值減去1000h 得到 12c0h,在偏移為12c0h處儲存的就是一個IMAGE_THUNK_DATA32陣列,他儲存的內容就是指向 IMAGE_IMPORT_BY_NAME 結構的地址,最後一個元素以一串0000 0000作為結束標誌,先來看一下IMAGE_THUNK_DATA32的定義規範。

typedef struct _IMAGE_THUNK_DATA32
{
    union
    {
        DWORD ForwarderString;
        DWORD Function;
        DWORD Ordinal;
        DWORD AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

直接使用WinHex定位到12c0h地址處,此處就是OrignalFirstThunk中儲存的INT的內容,如下圖,除去最後一個結束符00000000以外,一共有19個四位元組,則說明User32.dll中匯入了19API函數。

再來看一下FirstThunk也就是IAT中的內容,由於User32FirstThunk欄位預設值是209C,使用該值減去1000h即可得到109ch,此處就是IAT的內容,使用WinHex定位過去,可以發現兩者內容時完全一致的。

接著我們以第一個匯入RVA地址0000243Eh,用該值減去1000h得到143Eh,定位過去正好是EndDialog的字串,同樣的方式,第二個匯入RVA地址0000242ch,用該值減去1000h得到142ch 定位過去正好是PostQuitMessage的字串,如下圖綠色部分所示。

如上圖中我們已第二個函數PostQuitMessage為例,前兩個位元組0271h表示的是Hint值,後面的藍色部分則是PostQuitMessage字串,最後的0標誌結束標誌。

當程式被執行前,它的FirstThunk值與OrignalFirstThunk欄位都指向同一片INT中,此處我們使用LyDebugger工具對程式進行記憶體轉存,執行命令LyDebugger DumpMemory --path Win32Project.exe生成dump.exe檔案,該檔案則是記憶體中的映象資料。

當程式執行後,OrignalFirstThunk欄位不會發生變化,但是FirstThunk值的指向已經改變,系統在裝入記憶體時會自動將FirstThunk指向的偏移轉化為一個個真正的函數地址,並回寫到原始空間中,定位到dump.exe檔案FirstThunk 輸入表RVA地址處209Ch檢視,如下圖;

接著定位到OrignalFirstThunk處,也就是22c0h,觀察可發現,綠色的INT並沒有變化,但是黃色的IAT則相應的發生了變化

我們以IAT中第一個0x75f8ab90為例,使用x64dbg跟進一下,則可知是載入記憶體後EngDialog的記憶體地址。

當系統裝入記憶體後,其實只會用到IAT中的地址解析,輸入表中的INT就已經不需要了,此地址每個系統之間都會不同,該地址是作業系統動態計算後填入的,這也是為什麼會存在匯入表這個東西的原因,就是為了解決不同系統間的互通問題。

有時我們在脫殼時,由於IAT發生了變化,所以程式會無法被正常啟動,我們Dump出來的檔案由於使用的是記憶體地址,匯入表不一致所以也就無法正常執行,可以使用原始的未脫殼的匯入表地址對脫殼後的檔案匯入表進行覆蓋替換,以此來修復匯入表錯誤。

要實現這段程式碼,讀者可依次讀入脫殼前與脫殼後的兩個檔案,通過迴圈的方式將脫殼前的匯入表地址覆蓋到脫殼後的程式中,以此來實現對匯入表的修復功能,如下程式碼BuildIat則是筆者封裝首先的一個修復程式,讀者可自行體會其中的原理;

#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
#include <ImageHlp.h>
#pragma comment(lib,"Dbghelp")

DWORD RvaToFoa(PIMAGE_NT_HEADERS pImgNtHdr, LPVOID lpBase, DWORD dwRva)
{
  PIMAGE_SECTION_HEADER pImgSecHdr;
  pImgSecHdr = ImageRvaToSection(pImgNtHdr, lpBase, dwRva);
  return dwRva - pImgSecHdr->VirtualAddress + pImgSecHdr->PointerToRawData;
}

void BuildIat(char *pSrc, char *pDest)
{
  PIMAGE_DOS_HEADER pSrcImgDosHdr, pDestImgDosHdr;
  PIMAGE_NT_HEADERS pSrcImgNtHdr, pDestImgNtHdr;
  PIMAGE_SECTION_HEADER pSrcImgSecHdr, pDestImgSecHdr;
  PIMAGE_IMPORT_DESCRIPTOR pSrcImpDesc, pDestImpDesc;

  HANDLE hSrcFile, hDestFile;
  HANDLE hSrcMap, hDestMap;
  LPVOID lpSrcBase, lpDestBase;

  // 開啟原始檔與目標檔案
  hSrcFile = CreateFile(pSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hSrcFile == INVALID_HANDLE_VALUE)
    return;
  hDestFile = CreateFile(pDest, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hDestFile == INVALID_HANDLE_VALUE)
    return;

  // 分別建立兩份磁碟對映
  hSrcMap = CreateFileMapping(hSrcFile, NULL, PAGE_READONLY, 0, 0, 0);
  hDestMap = CreateFileMapping(hDestFile, NULL, PAGE_READWRITE, 0, 0, 0);

  // MapViewOfFile 設定到指定位置
  lpSrcBase = MapViewOfFile(hSrcMap, FILE_MAP_READ, 0, 0, 0);
  lpDestBase = MapViewOfFile(hDestMap, FILE_MAP_WRITE, 0, 0, 0);

  pSrcImgDosHdr = (PIMAGE_DOS_HEADER)lpSrcBase;
  pDestImgDosHdr = (PIMAGE_DOS_HEADER)lpDestBase;
  printf("[+] 原DOS頭: 0x%08X --> 目標DOS頭: 0x%08X \n", pSrcImgDosHdr, pDestImgDosHdr);

  pSrcImgNtHdr = (PIMAGE_NT_HEADERS)((DWORD)lpSrcBase + pSrcImgDosHdr->e_lfanew);
  pDestImgNtHdr = (PIMAGE_NT_HEADERS)((DWORD)lpDestBase + pDestImgDosHdr->e_lfanew);
  printf("[+] 原NT頭: 0x%08X --> 目標NT頭: 0x%08X \n", pSrcImgNtHdr, pDestImgNtHdr);

  pSrcImgSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)&pSrcImgNtHdr->OptionalHeader + pSrcImgNtHdr->FileHeader.SizeOfOptionalHeader);
  pDestImgSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)&pDestImgNtHdr->OptionalHeader + pDestImgNtHdr->FileHeader.SizeOfOptionalHeader);
  printf("[+] 原節表頭: 0x%08X --> 目標節表頭: 0x%08X \n", pSrcImgSecHdr, pDestImgSecHdr);

  DWORD dwImpSrcAddr, dwImpDestAddr;
  dwImpSrcAddr = pSrcImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  dwImpDestAddr = pDestImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  printf("[-] 原始IAT虛擬地址: 0x%08X --> 目標IAT虛擬地址: 0x%08X \n", dwImpSrcAddr, dwImpDestAddr);

  dwImpSrcAddr = (DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, dwImpSrcAddr);
  dwImpDestAddr = (DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, dwImpDestAddr);
  printf("[+] 匯入表原始偏移: 0x%08X --> 匯入表目的偏移: 0x%08X \n", dwImpSrcAddr, dwImpDestAddr);

  // 定位匯入表
  pSrcImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)dwImpSrcAddr;
  pDestImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)dwImpDestAddr;
  printf("[*] 定位原始匯入表地址: 0x%08X --> 定位目的匯入表地址: 0x%08X \n\n\n", pSrcImpDesc, pDestImpDesc);

  PIMAGE_THUNK_DATA pSrcImgThkDt, pDestImgThkDt;

  // 迴圈遍歷匯入表,條件是兩者都不為空
  while (pSrcImpDesc->Name && pDestImpDesc->Name)
  {
    char *pSrcImpName = (char*)((DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, pSrcImpDesc->Name));
    char *pDestImpName = (char*)((DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, pDestImpDesc->Name));

    pSrcImgThkDt = (PIMAGE_THUNK_DATA)((DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, pSrcImpDesc->FirstThunk));
    pDestImgThkDt = (PIMAGE_THUNK_DATA)((DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, pDestImpDesc->FirstThunk));
    printf("\n [*] 連結庫: %10s 原始偏移: 0x%08X --> 修正偏移: 0x%08X \n\n", pDestImpName, *pDestImgThkDt, *pSrcImgThkDt);

    // 開始賦值,將原始的IAT表中索引賦值給目標地址
    while (*((DWORD *)pSrcImgThkDt) && *((DWORD *)pDestImgThkDt))
    {
      DWORD dwIatAddr = *((DWORD *)pSrcImgThkDt);
      *((DWORD *)pDestImgThkDt) = dwIatAddr;
      printf("\t --> 源RVA: 0x%08X --> 拷貝地址: 0x%08X --> 修正為: 0x%08X \n", pSrcImgThkDt, pDestImgThkDt, dwIatAddr);
      pSrcImgThkDt++;
      pDestImgThkDt++;
    }
    pSrcImpDesc++;
    pDestImpDesc++;
  }
  UnmapViewOfFile(lpDestBase); UnmapViewOfFile(lpSrcBase);
  CloseHandle(hDestMap); CloseHandle(hSrcMap);
  CloseHandle(hDestFile); CloseHandle(hSrcFile);
}

void Banner()
{
  printf(" ____        _ _     _    ___    _  _____  \n");
  printf("| __ ) _   _(_) | __| |  |_ _|  / \\|_   _| \n");
  printf("|  _ \\| | | | | |/ _` |   | |  / _ \\ | |  \n");
  printf("| |_) | |_| | | | (_| |   | | / ___ \\| |  \n");
  printf("|____/ \\__,_|_|_|\\__,_|  |___/_/   \\_\\_|   \n");
  printf("                                           \n");
  printf("IAT 修正拷貝工具 By: LyShark \n");
  printf("Usage: BuildIat [脫殼前檔案] [脫殼後檔案] \n\n\n");
}

int main(int argc, char * argv[])
{
  Banner();
  if (argc == 3)
  {
    // 使用原始的IAT表覆蓋dump出來的映象
    BuildIat(argv[1], argv[2]);
  }
  return 0;
}

程式碼的使用很簡單,分別傳入脫殼前檔案路徑,以及脫殼後的路徑,則讀者可看到如下圖所示的輸出資訊,至此即實現了脫殼修復功能。

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