脫殼修復是指在進行加殼保護後的二進位制程式脫殼操作後,由於加殼操作的不同,有些程式的匯入表可能會受到影響,導致脫殼後程式無法正常執行。因此,需要進行修復操作,將脫殼前的匯入表覆蓋到脫殼後的程式中,以使程式恢復正常執行。一般情況下,匯入表被分為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;
我們以第一個呼叫動態連結庫為例,其地址與結構的說明如下所示:
每個IID結構的第四個欄位指向的是DLL
名稱的地址,以第一個動態連結庫為例,其RVA是0000 244A
將其減去1000h
得到檔案偏移144A
,跳轉過去看看,呼叫的是USER32.dll
庫。
上方提到的兩個欄位OrignalFirstThunk
和FirstThunk
都可以指向匯入結構,在實際裝入中,當程式中的OrignalFirstThunk
值為0時,則就要看FirstThunk
裡面的資料,FirstThunk常被叫做IAT
它是在程式初始化時被動態填充的,而OrignalFirstThunk
常被叫做INT
,它是不可改變的,之所以會保留兩份是因為,有些時候會存在反查的需求,保留兩份是為了更方便的實現。
在上述流程中,我們找到了User32.dll
的OrignalFirstThunk
,其地址為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
中匯入了19
個API
函數。
再來看一下FirstThunk
也就是IAT
中的內容,由於User32
的FirstThunk
欄位預設值是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 許可協定。轉載請註明出處!