2.9 PE结构:重建导入表结构
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程序恢复正常运行。一般情况下,导入表被分为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;
}
代码的使用很简单,分别传入脱壳前文件路径,以及脱壳后的路径,则读者可看到如下图所示的输出信息,至此即实现了脱壳修复功能。
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!