脫殼修複是指在進行加殼保護後的二進位程式脫殼操作後,由於加殼操作的不同,有些程式的導入表可能會受到影響,導致脫殼後程式無法正常運行。因此,需要進行修複操作,將脫殼前的導入表覆蓋到脫殼後的程式中,以使程式恢復正常運行。一般情況下,導入表被分為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
庫。
上方提到的兩個欄位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 許可協議。轉載請註明出處!
文章出處:https://www.cnblogs.com/LyShark/p/17686637.html
本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!