一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:460500587
微信公众号:计算机与网络安全
ID:Computer-network
导入地址表是PE文件结构中的一个表结构,在学习PE文件结构的时候虽然没有提到导入地址表,但是提到了数据目录。数据目录在IMAGE_OPTIONAL_HEADER中的DataDirectory中,我们回忆一下它的定义:
(1)NumberOfRvaAndSizes:该字段表示数据目录的个数,该个数的定义为16。如下:
(2)DataDirectory:数据目录表,由NumberOfRvaAndSize个IMAGE_DATA_DIRECTORY结构体组成。该数组中包含了输入表、输出表、资源等数据的RVA。IMAGE_DATA_DIRECTORY的定义如下:
该结构体的第一个变量为该目录的相对虚拟地址的起始值,第二个是该目录的长度。
一、导入表简介
在可执行文件中使用其他DLL可执行文件的代码或数据时称为导入,或者称为输入。当PE文件被加载时,Windows加载器会定位所有的导入的函数或数据。这个定位是需要借助于导入表来完成的。导入表中存放了使用的DLL的模块名称,及导入的函数。
在加壳与脱壳的研究中,导入表是非常关键的部分。加壳要尽可能地隐藏导入表,脱壳一定要找到导入表。如果无法还原或修复脱壳后的导入表的话,那么可执行文件仍然是无法运行的。
在免杀中也有与导入表相关的内容,比如移动导入表函数、修改导入表描述信息、隐藏导入表……不过这些操作都是杀毒软件将特征码定位到了导入表上才需要这样做,不过可以看出导入表也同样受到杀毒软件的“关注”。
二、导入表的数据结构定义
在数据目录中定位第二个目录,即IMAGE_DIRECTORY_ENTRY_IMPORT。该结构体中保存了导入函数的重要信息,每个DLL都对应一个IMAGE_IMPORT_DESCRIPTOR结构,也就是说导入的DLL文件与IMAGE_IMPORT_DESCRIPTOR是一对一的关系。IMAGE_IMPORT_DESCRIPTOR在文件中是一个数组,但是文件中并没有明确地指出导入表的个数,在导入表中是一个以全“0”的IMAGE_IMPORT_DESCRIPTOR为结束的。导入表对应的结构体定义如下:
(1)OriginalFirstThunk:该字段指向导入名称表的RVA,该表是一个IMAGE_THUNK_DATA的结构体数组。
(2)TimeDataStamp:该字段可以被忽略。
(3)ForwarderChain:该字段一般为0。
(4)Name:该字段为DLL名称的指针,该指针也为一个RVA。
(5)FirstThunk:该字段包含了导入地址表(IAT)的RVA,IAT是一个IMAGE_THUNK_DATA的结构体数组。
该结构体的成员是一个联合体,虽然联合体中有若干个变量,但由于该结构体中包含的是一个联合体,那么这个结构体也就相当于只有一个成员变量,只是有时代表的意义不同。
看其本质,该结构体实际上是一个DWORD类型。当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式导入,而这时第31位被看作是一个导入序号。当其最高位为0时,表示函数以函数名字符串的方式导入,这时DWORD的值表示一个RVA,并指向一个IMAGE_IMPORT_BY_NAME结构。
(1)Hint:该字段表示该函数在其DLL中的导出表中的序号。
(2)Name:该字段表示导入函数的函数名,导入函数是一个以ASCII编码的字符串,并以NULL来结尾。在IMAGE_IMPORT_BY_NAME中使用Name[1]来定义该字段,表示这是只有1个长度大小的字符串。通过越界访问,来达到访问变长字符串的功能。IMAGE_THUNK_DATA与IMAGE_IMPORT_DESCRIPTOR类似,同样是以一个全“0”的IMAGE_THUNK_DATA为结束的。
三、手动分析导入表
通过十六进制编辑器来学习导入表的结构体IMAGE_IMPORT_DESCRIPTOR。在这里随便找个PE(EXE格式)文件来进行分析,大家可以找一个PE文件来进行分析。
用C32ASM打开找来的PE文件,首先定位到数据目录的第二项,如图1所示。
图1 IMAGE_IMPORT_DESCRIPTOR的RVA及大小
在图1中看到了数据目录中的第二项的内容,其值分别是0x00024000和0x0000003C。0x00024000的值表示IMAGE_IMPORT_DESCRIPTOR的RVA,注意这里给出的是RVA。现在使用十六进制编辑器打开,那么就要通过RVA转换为FileOffset,也就是从相对虚拟地址转换为文件偏移地址。使用LordPE来进行转换,如图2所示。
图2 计算IMAGE_IMPORT_DESCRIPTOR的FileOffset
从图2中可以看出,0x00024000这个RVA对应的FileOffset为0x00023000。那么在C32中转移到0x00023000的位置处,按下Ctrl+ G组合键,在弹出的对话框中填入“23000”,如图3所示。
图3 C32中的“跳转到”
单击“确定”按钮,来到了文件偏移为00023000的位置处,如图4所示。
图4 导入表位置
来到文件偏移的00023000处就是IMAGE_IMPORT_DESCRIPTOR的开始位置了。从图4中可以看出,该文件有2个IMAGE_IMPORT_DESCRIPTOR结构体。我们重点分析第一个IMAGE_IMPORT_DESCRIPTOR,同样关于其他几个相关的数据结构也只分析第一个。
在IMAGE_IMPORT_DESCRIPTOR中只看最后两个字段,分别是Name和FirstThunk。看一下第一个IMAGE_IMPORT_DESCRIPTOR的这两个字段的值分别是00024338和00024190。这两个值都是RVA值。
先来看一下00024338这个值,该值表示DLL名字符串的RVA,将其转换为文件偏移后值为00023338。转到00023338这个文件偏移处查看,如图5所示。
图5 00023338处的内容
从图5中可以看出,这个位置保存的内容是一个字符串,该字符串的内容为KERNEL32.dll,说明这个Name值的确保存的是DLL名字符串的RVA。
再来看一下00024190这个值,该值表示IMAGE_THUNK_DATA的RVA。将该值转换为文件偏移后的值为00023190。转到00023190这个文件偏移处查看,如图6所示。
图6 FirstThunk的值
可以看到,000242E4是FristThunk的值。该值的最高位不为1,那么说该值是导入函数IMAGE_IMPORT_BY_NAME的RVA。将该值转换为文件偏移后的值为000232E4。转到000232E4这个偏移处查看该处的内容,如图7所示。
图7 IMAGE_IMPORT_BY_NAME处的内容
从图7中可以看出,这里保存的是CloseHandle()这个函数名称的函数字符串。
在IMAGE_IMPORT_DESCRIPTOR中,有两个IMAGE_THUNK_DATA结构体,第一个为导入名字表,第二个为导入地址表。两个结构体在文件当中是没有差别的,但是当PE文件被装载内存后,第二个IMAGE_THUNK_DATA的值会被修正,该值为一个RVA,该RVA加上映像基址后,虚拟地址就保存了真正的导入函数的入口地址。
四、枚举导入地址表
从上面的分析过程中已经学习了IMAGE_IMPORT_DESCRIPTOR这个结构体。那么,下面就来用代码实现枚举导入地址表的内容。我们知道一个DLL文件会对应一个IMAGE_IMPORT_DESCRIPTOR结构,而一个DLL文件中有多个函数,那么需要使用两个循环来进行枚举。外层循环来枚举所有的DLL,而内层循环来枚举所导入的该DLL的所有的函数名及函数地址。
代码如下:
只要对于手动分析导入表能够理解的话,那么上面这段代码就不难理解了。希望大家可以理解上面的代码。对某个程序进行一个测试,看其输出结果,如图8所示。
图8 测试程序的导入表信息
用OD进行验证一下,对该测试程序的导入表信息的获取是否正确。用OD载入测试程序,然后在数据窗口中按下Ctrl+ G组合键,输入地址“424190”,然后在数据窗口上单击鼠标右键,在弹出的菜单中选择“长型”->“地址”命令,看数据窗口的内容,如图9所示。
图9 测试程序在OD中的导入表信息
那么说明程序是正确的。关于导入表的知识就介绍到这里了。接下来,要介绍关于如何对IAT进行HOOK的内容了。
五、IAT HOOK介绍
在IMAGE_IMPORT_DESCRIPTOR中,有两个IMAGE_THUNK_DATA结构体,第一个为导入名字表,第二个为导入地址表(IAT)。两个结构体在文件当中是没有差别的,但是当PE文件被装载内存后,第二个IMAGE_THUNK_DATA的值会被修正,该值为一个RVA,该RVA加上映像基址后,虚拟地址就保存了真正的导入函数的入口地址。
在这个描述当中我们知道,要对IAT进行HOOK大概分为3个步骤,首先是获得要HOOK函数的地址,第二步是找到该函数所保存的IAT中的地址,最后一步是把IAT中的地址修改为HOOK函数的地址。这样就完成了IAT HOOK。也许这样的描述不是很清楚,那么下面就来举例说明一下。
比如要在IAT中HOOK系统模块kernel32.dll中的ReadFile()函数,那么首先是获得ReadFile()函数的地址,第二步是找到ReadFile()所保存的IAT地址,最后一步是把IAT中的ReadFile()函数的地址修改为HOOK函数的地址。下面通过一个实例来介绍IAT HOOK的具体过程和步骤。
六、IAT HOOK之CreateFileW()
这里对记事本进程的CreateFileW()函数进行IAT Hook。对CreateFileW()函数进行HOOK后主要是管控记事本要打开的文件是否允许被打开,我们一步一步地来完成代码。
先建立一个DLL文件,然后定义好DLL文件的主函数,并定义一个HookNotePadProcessIAT()函数,在DLL被进程加载的时候,让DLL文件去调用HookNotePadProcessIAT()函数。代码如下:
在遍历某程序的导入表时是通过文件映射来完成的,但是当一个可执行文件已经被Windows装载器装载入内存后,便可以省去CreateFile()、CreateFileMapping()等诸多繁琐的步骤,取而代之的是通过简单的GetModuleHandle()函数就可以得到EXE文件的模块映像地址,并能够很容易地获取DLL文件导入表的虚拟地址。代码如下:
在获得导入表的位置以后,要在导入表中找寻要HOOK函数的模块名,也就是说,要对CreateFileW()函数进行HOOK,首先要找到该进程中是否有“kernel32.dll”这个模块存在。一般情况下,kernel32.dll模块一定会存在于进程的地址空间内,因为它是Win32子系统的基本模块。当然,我们并不是简单地要找到该模块是否存在,关键是要找到这个模块所对应的IMAGE_IMPORT_DESCRIPTOR结构体,这样才能通过 kernel32.dll所对应的IMAGE_IMPORT_DESCRIPTOR结构体去查找保存CreateFileW()函数的地址,并进行修改。看一下代码:
对CreateFileW()函数进行HOOK,目的是为了对其打开的文件进行管控。由于这是演示程序,在G盘下建立一个test.txt文件,然后对其进行管控,也就是说,如果用记事本打开这个程序的话,可以选择性的是否允许打开,或者不允许打开。代码如下:
我们HOOK的函数是CreateFileW(),通过函数中的W可以看出,这个函数是一个UNICODE版本的字符串,也就是宽字符串。在CreateFileW()函数的参数中,lpFileName的类型是一个指向宽字符的指针变量。那么,就需要在操作该字符串时使用宽字符集的字符串函数,而不应该再使用操作ANSI字符串的函数。在代码中wcscpy()、wcscmp()、wcslwr()都是针对宽字符集的字符串。WCHAR是定义宽字符集类型的关键字。L"g:\\test.txt"中的“L”表示这个字符串常量是一个宽字符型的。
打开一个记事本程序,然后将编译连接好的DLL文件注入到记事本进程中,当注入并HOOK成功后会用对话框提示“Hook Successfully !”。然后用记事本打开G盘下的test.txt文件,会弹出对话框询问“是否打开文件”,单击“否”按钮,也就是拒绝打开该文件,如图10和图11所示。
图10 询问是否打开
图11 选择“否”后的提示
在图11中单击“确定”按钮,然后可以看到记事本并没有打开G盘下的test.txt文件,这说明对G盘下的test.txt文件的管控还算是成功的。
以上实例演示了如何对IAT进行HOOK。不过上面针对IAT进行HOOK的做法只能针对隐型调用。也就是说,可执行文件是直接调用了DLL的导出函数,用上面的代码可以对IAT进行HOOK。如果是显式调用的话,以上的例子就无法达到HOOK的作用了。当可执行文件直接通过调用LoadLibrary()函数和GetProcAddress()函数来使用某个函数的话,上面的HOOK代码是无能为力的。如何解决这样的问题,答案是要对LoadLibrary()和GetProcAddress()函数也进行HOOK,这样就可以避免对DLL的显式加载,和对函数的显式调用了。
微信公众号:计算机与网络安全
ID:Computer-network
【推荐书籍】