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

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

脫殼修複是指在進行加殼保護後的二進位程式脫殼操作後,由於加殼操作的不同,有些程式的導入表可能會受到影響,導致脫殼後程式無法正常運行。因此,需要進行修複操作,將脫殼前的導入表覆蓋到脫殼後的程式中,以使程式恢復正常運行。一般情況下,導入表被分為IAT(Import Address Table,導入地址表... ...


脫殼修複是指在進行加殼保護後的二進位程式脫殼操作後,由於加殼操作的不同,有些程式的導入表可能會受到影響,導致脫殼後程式無法正常運行。因此,需要進行修複操作,將脫殼前的導入表覆蓋到脫殼後的程式中,以使程式恢復正常運行。一般情況下,導入表被分為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 許可協議。轉載請註明出處!

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

-Advertisement-
Play Games
更多相關文章
  • 作者:是奉壹呀 \ 來源:juejin.cn/post/7262274383287500860 看到一個評論,裡面提到了list.sort()和list.strem().sorted()排序的差異。 說到list sort()排序比stream().sorted()排序性能更好,但沒說到為什麼。 ! ...
  • Sermant是基於Java位元組碼增強技術的無代理服務網格,其利用Java位元組碼增強技術為宿主應用程式提供服務治理功能。 ...
  • 京東茅臺搶購腳本可以分為以下幾部分,具體實現步驟如下: 登錄京東賬號 首先需要登錄京東賬號。一個簡單的方式是使用Python的 selenium 庫。在使用 selenium 庫前,需要安裝 selenium 庫和對應的瀏覽器驅動。 示例代碼如下所示: from selenium import we ...
  • # It can explain what ? 如下是解釋器要解釋的主體: - 加減乘除等運算,3+4/9+6*8 - 摩爾斯電碼 - 正則表達式 - El表達式 - OGNL表達式 - 小明是北京人 - 小紅是一名售貨員 - 部門領導下發一則通知 - ... # How explain ? 解釋器 ...
  • # ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98edff345fb44c9ca30237fa7958f6f8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1920&h=1080&s=72 ...
  • 大家好,我是 god23bin,在日常開發中,我們經常需要處理日期和時間,日期和時間可以說是一定會用到的,現在總結下 Java 中日期與時間的基本概念與一些常用的用法。 ...
  • centos7.9版本 1.下載FTP離線安裝包: http://rpmfind.net/linux/rpm2html/search.php?query=vsftpd(x86-64) 選擇最後一個 vsftpd-3.0.2-28.el7.x86_64.rpm 2.檢查是否已經安裝了vsftp rpm ...
  • 上次我們聊到 CLI 的領域交互模式。在領域交互模式中,可能存在多層次的子命令。在使用過程中如果全評記憶的話,命令少還好,多了真心記不住。頻繁 --help 也是個很麻煩的事情。如果每次按 'tab' 鍵就可以提示或補齊命令是不是很方便呢。這一節我們就來說說 'autocommplete' 如何實現... ...
一周排行
    -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# ...