2.9 PE结构:重建导入表结构
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后由于加壳操作的不同有些程序的导入表可能会受到影响导致脱壳后程序无法正常运行。因此需要进行修复操作将脱壳前的导入表覆盖到脱壳后的程序中以使程序恢复正常运行。一般情况下导入表被分为IATImport Address Table导入地址表和INTImport 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 许可协议。转载请注明出处
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |