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
  • 移動開發(一):使用.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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...