脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程序恢复正常运行。一般情况下,导入表被分为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 许可协议。转载请注明出处!

2.9 PE结构:重建导入表结构的更多相关文章

  1. 零基础逆向工程22_PE结构06_导入表

    导入表结构 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstTh ...

  2. PE知识复习之PE的绑定导入表

    PE知识复习之PE的绑定导入表 一丶简介 根据前几讲,我们已经熟悉了导入表结构.但是如果大家尝试过打印导入表的结构. INT IAT的时候. 会出现问题. PE在加载前 INT IAT表都指向一个名称 ...

  3. PE文件 01 导入表

    0x01  导入表结构  数据目录表中的第二个成员标记了导入表的RVA和Size大小,由此可以定位到导入表: typedef struct _IMAGE_DATA_DIRECTORY { DWORD ...

  4. mysql导出/导入表结构以及表数据

    导出: 命令行下具体用法如下:  mysqldump -u用戶名 -p密码 -d 数据库名 表名 脚本名; 1.导出数据库为dbname的表结构(其中用戶名为root,密码为dbpasswd,生成的脚 ...

  5. 用命令从mysql中导出/导入表结构及数据

    在命令行下mysql的数据导出有个很好用命令mysqldump,它的参数有一大把,可以这样查看:mysqldump最常用的:mysqldump -uroot -pmysql databasefoo t ...

  6. mysql-用命令导出、导入表结构或数据

    1. 导出整个数据库(表结构和数据) mysqldump -u用户名 -p  数据库名 > 导出的文件名 [root@localhost work]# mysqldump -uroot -p m ...

  7. mysql导入数据库_仅仅用frm向mysql导入表结构

    网上一个连接mysql的jsp代码段,给了数据库的备份文件.可是仅仅有frm, mysql的每张表有三个文件.各自是,*.frm是描写叙述了表的结构.*.MYD保存了表的数据记录.*.MYI则是表的索 ...

  8. 利用PE数据目录的导入表获取函数名及其地址

    PE文件是以64字节的DOS文件头开始的(IMAGE_DOS_HEADER),接着是一段小DOS程序,然后是248字节的 NT文件头(IMAGE_NT_HEADERS),NT的文件头位置由IMAGE_ ...

  9. Oracle表结构转换SqlSERVER表结构 脚本

    在审计工作中,有时需要将Oracle的表结构修改后再SqlSERVER中创建表结构,然后将数据导入到SqlSERVER中,在修改表结构的过程中方法狠多.手工修改,最蠢的方法,或者用工具UE批量修改,还 ...

  10. [PE结构分析] 8.输入表结构和输入地址表(IAT)

    在 PE文件头的 IMAGE_OPTIONAL_HEADER 结构中的 DataDirectory(数据目录表) 的第二个成员就是指向输入表的.每个被链接进来的 DLL文件都分别对应一个 IMAGE_ ...

随机推荐

  1. 代码随想录算法训练营Day18 二叉树|  654.最大二叉树  617.合并二叉树  700.二叉搜索树中的搜索  98.验证二叉搜索树

    654.最大二叉树 题目链接:654.最大二叉树 给定一个不重复的整数数组 nums . 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点,其值为 nums 中的最大值. 递归地 ...

  2. 一分钟学一个 Linux 命令 - ls

    前言 大家好,我是 god23bin.今天我给大家带来的是 Linux 命令系列,每天只需一分钟,记住一个 Linux 命令不成问题.今天,我们要介绍的是一个常用而又强大的命令:ls(list). 什 ...

  3. 商业智能 (BI) 对企业中每个员工的 5 大好处

    本文由葡萄城技术团队于博客园原创并首发.葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 众所周知,商业智能 (BI) 是探索企业数据价值的强大工具,能够帮助企业做出明智的决策.提高绩效 ...

  4. CompTIA Pentest+

    关于学习后CompTIA Pentest+笔记 渗透测试工具 讲述了nmap,burp Suite,Metasploit,Nessus,hydra的入门使用 nmap:https://www.cnbl ...

  5. NOIP模拟测试A3 赛后总结

    T1 谜之阶乘 可以发现题目要求我们求的实际上是若干个连续整数 \(c_i\) ,使得 \(\displaystyle \prod c_i = n\),通过打表可以发现这些连续整数的长度 \(d\) ...

  6. TVM Deploy Runtime[施工中]

    本文地址:https://www.cnblogs.com/wanger-sjtu/p/17291070.html tvm 中在部署时有多个选择,最开始的graph exectuor runtime . ...

  7. XXE漏洞详解

    XML外部实体注入--XXE漏洞详解 简单来说一下这个XXE漏洞,在这之前我也阅读了很多关于XXE漏洞的文章,发现有一小部分文章题目是 "XXE外部实体注入" 这样的字眼,我想这样 ...

  8. python打包exe总结 pyinstaller py2exe

    Python打包exe 有很多可以用的 如 pyinstaller py2exe cx_Freeze nuitka py2app py0xidizer 其中cx_Freeze没用过 nuitka是把p ...

  9. 关于SQL SERVER ROW_NUMBER(),RANK(),DENSE_RANK() 的排序和分页查询问题

    经常接触SQL SERVER 的朋友来说,排序是经常遇见的问题,有的人还会傻傻自己写排序,比如用循环去写,当然这就比较难受 今天就给大家介绍一下SQL SERVER 自带的排序,共有三种,分别为ROW ...

  10. 每日一题 力扣 1090 https://leetcode.cn/problems/largest-values-from-labels/

    每日一题 力扣 1090 https://leetcode.cn/problems/largest-values-from-labels/ 先对这道题目进行排序,贪心一下,要求分数最高的放在前面,而标 ...