返回介绍

第三十三章 - 神马是 IAT 如何修复

发布于 2025-01-31 21:06:59 字数 15375 浏览 0 评论 0 收藏 0

在介绍如何修复 IAT 之前,我们首先来介绍一下 IAT 的相关基本概念,本章的实验对象依然是 Cruehead 的 CrackMe。首先我们来定位该程序的 IAT 位于何处,然后再来看看对其加了 UPX 的壳后,IAT 又位于何处。

什么是 IAT:

我们知道每个 API 函数在对应的进程空间中都有其相应的入口地址,例如:我们用 OD 加载 Cruehead 的 CrackMe,在命令栏中输入? MessageBoxA

大家可以看到在我的机器上,MessageBoxA 这个 API 函数的地址为 77D504EA,如果大家在自己的机器上面定位到这个地址的话,可能有部分人的机器上该地址对应的还是 MessageBoxA 的入口地址,而另外一部分人的机器上该地址对应的就不是 MesageBoxA 的入口地址了,这取决于大家机器的操作系统版本,以及打补丁的情况。众所周知,操作系统动态库版本的更新,其包含的 API 函数入口地址通常也会改变。

比如 User32.dll

我们就拿 Cruehead 的 CrackMe 中的 MessageBoxA 这个 API 函数来说吧,其入口地址为 77D504EA,在我的机器上运行的很好,那些跟我操作系统版本以及 User32.dll 版本相同的童鞋的机器上该程序运行可能也很正常,但是如果在操作系统版本或者 User32.dll 的版本跟我的不同童鞋的机器上运行,可能就会出错。

为了解决以上兼容问题,操作系统就必须提供一些措施来确保该 CrackMe 可以在其他版本的 Windows 操作系统,以及 DLL 版本下也能正常运行。

这时 IAT(Import Address Table:输入函数地址表) 就应运而生了。

大家不要觉得其名字很霸气,就会问是不是很难?其实不然。接下来我们一起来探讨一下如何在脱壳过程中定位 IAT。

我们现在通过在反汇编窗口中单击鼠标右键选择-Search for-All intermodular calls 来看看主模块中调用了哪些模块以及 API 函数。

这里我们可以看到有几处调用了 MessageBoxA,我们在第一个 MessageBoxA 调用处双击鼠标左键。

反汇编窗口就会马上定位到该调用处,OD 提示窗口中显示其实际调用的是 40143A 处的 JMP.&USER32.MessageBoxA,这里用尖括号括起来了,说明这里是直接调用,而非间接调用。

这里其实就是 CALL 40143A,显示为 CALL <JMP.&USER32.MessageBoxA>大家可能会觉得不太直观。这里我们打开 Debugging options 菜单项:

Disasm 标签页中的 Show sysmbolic addresses 选项被勾选上了,如果我们去掉该对勾,将不会显示函数地址。

我们可以看到右边的注释窗口中同样显示了 API 函数的参数以及函数名称,比刚刚显示符号地址看起来更直观,一眼就可以看出是一个直接调用。

CALL 40143A

在 Search for-All intermodular calls 窗口中显示如下:

我们可以看到这里有三处通过 CALL 40143A 调用 MessageBoxA,我们定位到 40143A 处看看是什么。

这里我们可以看到是一个间接跳转。即

JMP [4031AC]

这里我们再次勾选上显示符号地址的选项,可以更加直观的看出其调用的 API 函数。

这里有意思的地方就来了,我们看到 JMP [4031AC](4031AC 这个内存单元中保存的数值才是 MessageBoxA 真正的入口地址)。我们还可以看到很多类似的间接 JMP。

这就是为了解决各操作系统之间的兼容问题而设计的,当程序需要调用某个 API 函数的时候,都是通过一个间接跳转来调用的,读取某个地址中保存的 API 函数地址,然后调用之。我们现在在数据窗口中定位到 4031AC 地址处,看看该内存单元中存放的是什么。

这里我们可以看到,4031AC 中保存的是 77D504EA,这一片区域包含了该程序调用的所有 API 函数的入口地址,这块区域我们称之为 IAT(导入函数地址表),这里就是解决不同版本操作系统间调用 API 兼容问题的关键所在,该程序在不同版本操作系统上都是调用间接跳转到 IAT 表中,在 IAT 中读取到真正的 API 函数入口地址,然后调用之,所以说只需要将不同系统中的 API 函数地址填充到 IAT 中,这样就可以确保不同版本系统调用的都是正确的 API 函数。

有些人可能会问,4031AC 这个地址在不同机器上也可能会不同的吧?

呵呵,这个问题提的非常好,我们一起来看看操作系统将正确的 API 函数入口地址填充到 IAT 中的具体原理,大家就会明白了。

这里我们选中 4031AC 中保存的内容,单击鼠标右键选择-View executable file,就能看到 4031AC 这个虚拟地址对应于可执行文件中的文件偏移是多少了。

我们可以看到在可执行文件对应文件偏移处中的内容为 60 33 00 00,当程序运行起来的时候,0FAC 这个文件偏移对应的虚拟地址处就会被填充为 EA 04 D5 77,也就是说该 CrackMe 进程空间中的 4031AC 地址处会被填入正确的 API 函数地址。

有这么神奇?

Windows 操作系统当可执行文件被加载到进程所在内存空间中时,会将正确的 API 函数地址填充到 IAT 中,这里就是 4031AC 中被填入了 MessageBoxA 的入口地址,其他 IAT 项也会被填入对应的 API 函数地址。

其实操作系统并没有大家想象得的那么神奇,我们看到 0FAC 文件偏移处的值 3360,该数值其实是 RVA(相对虚拟地址),其指向对应的 API 函数名称。

这里 3360 加上映像基址即 403360,我们定位到 403360 处,看看是什么。

这里我们可以看到指向的是 MessageBoxA 这个字符串,也就是说操作系统可以根据这个指针,定位到相应的 API 函数名称,然后通过调用 GetProcAddress 获取对应 API 函数的地址,然后将该地址填充到 IAT 中,覆盖原来的 3360。这样就能保证在程序执行前,IAT 中被填充了正确的 API 函数地址。如果我们换一台机器,定位到 4031AC 处,可能会看到里面存放着不同的地址。

JMP [4031AC]

这样就能够调用 MessageBoxA 了,大家可能会觉得这个过程很复杂,其实填充 IAT 的过程都是操作系统帮我们完成的,在程序开始执行前,IAT 已经被填入了正确的 API 函数地址。

也就是说,为了确保操作系统将正确的 API 函数地址填充到 IAT 中,应该满足一下几点要求:

1:可执行文件各 IAT 项所在的文件偏移处必须是一个指针,指向一个字符串。

2:该字符串为 API 函数的名称。

如果这两项满足,就可以确保程序在启动时,操作系统会将正确的 API 函数地址填充到 IAT 中(后面会详细介绍操作系统是如何填充 IAT 的)。

假如,我们当前位于被加壳程序的 OEP 处,我们接下来可以将程序 dump 出来,但是在 dump 之前我们必须修复 IAT,为什么要修复 IAT 呢?难道壳将 IAT 破坏了吗?对,的确是这样,壳压根不需要原程序的 IAT,因为被加壳程序首先会执行解密例程,读取 IAT 中所需要的 API 的名称指针,然后定位到 API 函数地址,将其填入到 IAT 中,这个时候,IAT 中已经被填充了正确的 API 函数地址,对应的 API 函数名称的字符串已经不需要了,可以清除掉。

大部分的壳会将 API 函数名称对应的字符串以密文的形式保存到某个地址处,让 Cracker 们不能那么容易找到它们。

下面我们来看看 CrackMe UPX 这个程序,在 dump 之前我们需要修复 IAT。

我们定位到 4031AC 处-原程序 MessageBoxA 入口地址的存放处。

是空的,那么 403360 指向的字符串呢?

也是空的,我们跟到 OEP 处,再来看看这几个地址处有没有内容,我们知道原程序在运行之前,IAT 必须被填充上正确的 API 函数地址。

JMP [4031AC]

如果此时 IAT 还是空的话,那么程序运行起来就会出错,我们现在定位到 OEP。

我们在这个 JMP OEP 指令处设置一个断点,运行起来,接着来看看 IAT:

我们可以看到壳的解密例程已经将正确的 API 函数地址填充到原程序的 IAT 中,如果这个时候我们将程序 dump 出来的话,运行会出错,因为 dump 出来的程序启动所必须的数据是不完整的。

我们现在来看看各个 API 函数名称,定位到 403360 处,会发现是空的。

现在我们 dump 出来看看,dump 出来的原程序代码肯定是正确的,但是程序仍然无法正常运行,因为缺少数据,操作系统无法填充 IAT。

Dump 的话我们需要用到一个工具,名字叫做 LordPE(PS:大家应该用的很多吧)。

我们运行 LordPE,定位到需要 dump 的 CRACKME UPX 所在的进程,当前该进程处于 OEP 处。

选中 CRACKME UPX 所在的进程。

我们单击鼠标右键选择-active dump engine-IntelliDump-Select!。接着选择 dump full。

我们将 dump 出来的程序命名为 dumped.exe。

如果我们直接运行 dumped.exe 的话会发现无法启动,尝试用 OD 加载 dumped.exe,OD 会报错,我们来看看日志窗口中的错误信息。

这里我们机器上提示错误发现在 7C929913 地址处,我们定位到该地址(大家可以根据自己机器上显示的错误地址自行定位)。

这里我们可以给这一行设置一个硬件执行断点或者 INT 3 断点,即当断在这一行时看看错误发生之前是什么状况。

我们运行起来,会发现没有断在这一行,这是因为勾选了忽略异常选项的缘故,这里我们去掉忽略异常选项的对勾,重新运行起来。

断了下来,我们可以看到该错误是在到达入口点之前产生的,所以 dumped.exe 无法正常运行,我们现在来看看 IAT 的情况。

我们可以看到当前虽然在我的机器上各个 API 函数的地址被填充到 IAT 中,但是想要正常运行在其他机器上的话,必须要指向各个 API 函数名称字符串的指针,这样才是确保操作系统能够通过 GetProcAddress 获取到正确的 API 函数地址并填充到 IAT 中。

这里该 dumped.exe 缺少这些指向 API 函数名称字符串的指针,所以运行的时候会发生错误。

这里大家不要尝试先 dump 出来,然后再恢复各个 API 函数的名称字符串以及其指针,如果这样手工修复的话,是一件极其困难的工作,你需要将 4031AC 地址处的内容修改为 MessageBoxA 这个字符串的指针,IAT 中的其他项也要进行相应的处理。

比较明智的做法是,dump 出来之前就将 IAT 修复了。

我们知道 dump 出来的代码肯定是正确的,我们定位到 401000 处看一看。

我们看到 API 函数的调用处,40135C 地址处应该是调用的 MessageBoxA。

我们定位到 40143A 处,这里依然是通过一个间接跳转。

这些间接跳转是无法正常运行的,因为在正常情况,操作系统必须知道指向各个 API 函数名称字符串的指针,然后通过 GetProcAddress 定位到各个 API 函数正确的入口地址并填充到 IAT 中,这样这些间接跳转才能起作用。

下面我们来看看未加壳程序的 IAT。

我们用 OD 加载 Cruehead 的 CrackMe。

我们来定位该 CrackMe PE 结构中一些重要字段。首先在数据窗口中定位 400000 地址处。

单击鼠标右键选择-Special-PE header 切换到 PE 头的显示模式。

往下拉。

我们可以看到 PE 头的偏移为 100。

即 PE 头位于 400100 地址处。

继续往下拉,我们可以看到 IT(导入表) 的指针,这里大家不要将其跟 IAT 搞混淆了。

IT = 导入表

IAT = 输入函数地址表

我们知道当程序启动之前操作系统会将各个 API 函数的地址填充到 IAT 中,那么 IT(导入表) 又是怎么一回事呢?首先我们定位到导入表,该导入表偏移值为 3000(即虚拟地址为 403000),长度为 670(十六进制),即 403670 为导入表的结尾。我们一起来看一看。

我们将数据窗口的显示模式切换为正常状态。

这就是导入表了,我们来介绍一下导入表的结构吧。

我们选中的这 20 个字节是导入表的描述符结构。官方的叫法为 IMAGE_IMPORT_DESCRIPTOR。每组为 20 个字节,IMAGE_IMPORT_DESCRIPTOR 包含了一个的字符串指针,该指针指向了某个的动态链接库名称字符串。

我们来看个例子:

这里我们将 IMAGE_IMPORT_DESCRIPTOR 简称为 IID。这里选中的部分为导入表中的第一个 IID。其中 5 个 DWORD 字段的含义如下:

OriginalFirstThunk

TimeDateStamp 时间戳

ForwarderChain 链表的前一个结构

Name1 指向 DLL 名称的指针

FirstThunk 指向的链表定义了针对 Name1 这个动态链接库引入的所有导入函数

前三个字段不是很重要,对于我们 Cracker 来说,我们只对第 4,5 字段感兴趣。

正如大家所看到的,第 4 个字段为指向 DLL 名称字符串的指针,我们来看看 403290 处是哪个 DLL 的名称。

这里我们可以看到是 USER32.DLL,第 5 个字段指向了 USER32.DLL 对应 IAT 项的起始地址,即 403184。

这里就是 IAT 了,导入表的结束地址为 403670。导入表中的每个 IID 项指明了 DLL 的名称以及其对应 IAT 项的起始地址。紧凑的排列在一起,供操作系统使用。

大量实验表明,IAT 并不一定位于在导入表中。IAT 可以位于程序中任何具有写权限的地方,只要当可执行程序运行起来时,操作系统可以定位到这些 IID 项,然后根据 IAT 中标明的 API 函数名称获取到函数地址即可。下面我们来总结一下操作系统填充 IAT 的具体步骤:

1:定位导入表

2:解析第一个 IID 项,根据 IID 中的第 4 个字段定位 DLL 的名称

3:根据 IID 项的第 5 个字段 DLL 对应的 IAT 项的起始地址

4:根据 IAT 中的指针定位到相应 API 函数名称字符串

5:通过 GetProcAddress 获取 API 函数的地址并填充到 IAT 中

6:当定位到的 IAT 项为零的时候表示该 DLL 的 API 函数地址获取完毕了,接着继续解析第二个 IID,重复上面的步骤。

下面我们来手工的体验一下这个步骤:

1) 定位导入表

2) 定位到导入表的起始地址

3) 根据第一个 IID 项中的第四个字段得到 DLL 名称字符串的指针,这里指向的是 USER32.DLL

根据第五个字段的内容定位到 IAT 项的起始地址,这里是 403184,我们定位到该地址处。

这里我们可以看到已经被填充了正确的 API 函数的入口地址,跟我们 dump 出来的结果一样,我们再来看看相应的可执行文件偏移处的内容是什么。

这里我们可以看到第一个 API 函数的名称位于 4032CC 地址处,我们定位到该地址处。

第一个 API 函数是 KillTimer,我们在 OD 中看到的 KillTimer 的入口地址是操作系统调用 GetProcAddress 获取到的。

这里我们可以看到 KillTimer 的入口地址为 77D18C42。该地址将被填充到 IAT 相应单元中去覆盖原来的值。

这里是 IAT 中的第一元素。

我们再来看下一个元素,向后偏移 4 就是,来看一看该 API 函数名称字符串的指针是多少。

定位到可执行文件的相应偏移处:

32D8 即 4032D8,来看看该 API 函数的名称是什么,这里由于该指针不为零,说明该 API 函数还是位于 USER32.DLL 中的。

这里我们可以看到第二个 API 函数是 GetSystemMetrics,通过该函数名称可以通过 GetProcAddress 获取到其函数地址然后填充到 IAT 中。接下来按照以上步骤依次获取 USER32.DLL 中的其他的函数地址,直到遇到的 IAT 项为零为止。我们来看一看可执行文件中结束项位于哪里。

我们可以看到当 IAT 中元素为零的时候表明 USER32.DLL 就搜索完毕了,我们接着来看下一个 IID。

这里我们根据第 4,5 字段分别可以知道第二个 DLL 的名称,以及对应 IAT 项的起始地址。

DLL 的名称字符串位于 40329B 地址处。

我们可以看到第二个 DLL 为 KERNEL32.DLL,该 DLL 对应的 IAT 项起始地址为 40321C。

这里我们可以看到前一个 DWORD 是零,表示 USER32.DLL 的 API 函数的结尾。40321C 表示 KERNEL32.DLL 的 API 函数地址项的开始。

根据这些指针我们就可以定位到 kernel32.dll 中的各个 API 函数名称字符串,进而获取到其函数入口地址,接着填充到对应的 IAT 项中覆盖原来的内容。

本章这里就结束了,我给大家描述了 IAT 被填充的整个过程,了解这个过程对大家来说是很有必要的,这部分内容是重建 IAT 必备的基础知识,大家只有理解了其基本原理,然后配上适当的工具,就可以方便进行 IAT 的修复工作了。

好了,下一章我们将介绍具体如何修复 IAT。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文