在远程进程中注入DLL钩挂IAT的方法
2014-08-23张钟
张 钟
(重庆理工大学,重庆 400054)
0 引言
在Windows系统的编程应用中,有时需要对某些API函数进行拦截,在拦截函数里执行自己的代码功能后,再返回原函数执行或者改变原函数操作行为等。拦截简称为钩子,可分为内核级和用户级钩子,内核级钩子有SYSENTER钩子、SSDT钩子、内联钩子、IRP钩子、LADDR 钩子、IDT 钩子、IOAPIC 钩子[1]等,实现起来较难,须具有0环权限;用户级钩子3环权限就可实现。用户级拦截API函数常用的方法有2种:一种是内联钩子,一种是导入地址表(Import Address Table,IAT)钩子。内联钩子的特点是将被拦截函数的首部的5个字节先保存起来,然后修改为JMP+钩子函数的偏移地址,待钩子函数代码执行完后,再将保存起来的5个字节写回原函数的首部,然后再执行原函数。这种方法的缺点是:(1)对硬件CPU有依赖性,不同的CPU其JMP指令是不同的;(2)在抢占式、多线程环境下根本不能工作。因为一个线程覆盖另一个函数起始位置的代码是需要时间的,在这个过程中,另一个线程可能调用同一函数,其结果将是灾难性的[2]。而导入地址表钩子不存在上述2个问题,方法简单,容易实现,而且程序也相当健壮。此外,在软件的加密、解密和反病毒应用中也往往涉及PE文件的IAT。当Windows装载器把PE文件装入内存时,会根据PE文件头中的导入表所记录的DLL名称,将DLL装入内存,以供PE文件使用DLL中的函数代码来实现程序的各种功能。当程序调用API函数时会首先在导入表中的IAT中查找该函数入口地址,找到后调用该函数。PE文件的导入表可分为2种结构形式:(1)导入表磁盘映像,可供查看导入表所记录的DLL名称和导入函数名称或序号;(2)导入表的内存映像,适用于钩挂导入地址表IAT。本文在分析阐明导入表的内存数据结构基础上,重点阐明导入地址表(IAT)与钩子函数关联起来的钩子原理:直接钩挂和间接钩挂,据此原理编程实现钩挂IAT的钩子模块。
1 钩挂IAT的原理与编程实现
1.1 导入表的数据结构
图1 PE文件装入内存中的导入表结构(部分)
要钩挂IAT首先要掌握导入表的内存映像结构,图1是PE文件装入内存中的导入表的部分结构。PE文件的导入表是由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成的数组,每一个IMAGE_IMPORT_DESCRIPTOR结构对应一个DLL,导入表的最后由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构结束。该结构的定义如下:
字段OriginalFirstThunk所指的导入名称表(Import Name Table,INT)由若干个 IMAGE_THUNK_DATA结构组成的数组,每一个IMAGE_THUNK_DATA结构对应一个API导入函数,数组的最后由一个内容全为0的IMAGE_THUNK_DATA结构结束。有的链接器产生的 PE文件没有 INT,如 Borland公司的TLINK,因此由Borland生成的PE文件是不能绑定的[3],但这不影响钩挂IAT。该结构的定义如下:
从这个结构的定义可看到,该结构是一个共用体,实际上就是一个双字。当双字的最高位是1时,表示函数是以序号导入的,低31位就是函数的序号值;当最高位是0时,表示函数是以函数名称(ANSI字符串,以0结尾)导入的,双字表示是一个RVA,此时指向一个IMAGE_IMPORT_BY_NAME结构。IMAGE_IMPORT_BY_NAME结构定义如下:
1.2 钩挂IAT的原理
(1)直接法钩挂。在图1中的IMAGE_IMPORT_DESCRIPTOR结构中,字段FirstThunk指向导入地址表(IAT),导入地址表中的每一项都对应一个导入函数的入口地址,用钩子函数的地址取代IAT中导入函数的入口地址即可实现钩挂。
(2)间接法钩挂。在图1中,字段FirstThunk同时也指向IMAGE_THUNK_DATA结构数组,在内存中IMAGE_THUNK_DATA结构数组就是导入地址表IAT,该结构中的共用体字段成员变量u1.Function的值是与IAT共用的同一个导入函数的入口地址,用钩子函数的地址取代成员变量u1.Function的值即可实现钩挂,并自动同步取代IAT中对应导入函数的入口地址。
在查看内存中的导入地址表时,看到的只是一个IAT。但用编程的方法去钩挂IAT时,则可选择字段FirstThunk指向的2条路径之一去钩挂IAT。
1.3 钩挂IAT的编程实现
要钩挂IAT,首先要定位内存中导入表的位置,然后由IMAGE_IMPORT_DESCRIPTOR结构中的字段FirstThunk找到IAT,再置换导入函数在IAT中对应的入口地址(直接法)或者置换成员变量u1.Function的值(间接法)即可。定位IAT位置的方法有3种。
(1)从PE基地址开始将其定位到PE头的IMAGE_NT_HEADERS结构体,由结构中的字段名OptionalHeader再定位到 IMAGE_OPTIONAL_HEADER32结构体,由该结构中的字段名DataDirectory所指的第2个IMAGE_DATA_DIRECTORY结构体,由该结构中的字段名VirtualAddress所指即是导入表IMAGE_IMPORT_DESCRIPTOR结构体数组,通过该结构的字段名FirstThunk即指向IAT的RVA。
(2)通过 IMAGE_OPTIONAL_HEADER32结构体中的字段名DataDirectory所指的第13个IMAGE_DATA_DIRECTORY结构体,由该结构中的字段名VirtualAddress直接指向IAT的RVA。
(3)使用imagehlp.dll动态链接库中的函数ImageDirectoryEntryToData,该函数的返回值即是指向导入表IMAGE_IMPORT_DESCRIPTOR结构体数组的VA,通过该结构的字段名FirstThunk即指向IAT的RVA。
本文采用方法(1)并结合图1的导入表的内存结构,用Win32汇编语言实现直接法钩子模块Hook-PEIAT用于钩挂IAT,可用于钩挂导入表中指定的DLL中的指定API导入函数;此外,为测试直接法和间接法实现的钩子模块的钩挂效果,所要注入远程进程中的DLL模块必须包括钩子模块和钩子函数模块,因为钩挂IAT的目的就是为了实现钩子函数的代码功能。
按图1所示的字段FirstThunk指向的间接法去钩挂IAT,可在钩子模块HookPEIAT中用下面程序代码(Ⅱ)置换代码(Ⅰ)即可实现。
现在用C或VC++编写的钩挂IAT的程序代码都是采用间接法实现导入地址表钩子。这是因为C和VC++操作汇编指令比较麻烦,而操作变量和取变量地址却很容易。
2 测试实验结果及分析
在 Windows 7 旗舰版,CPU:Core i3,2.2 GHz和Windows XP SP2,CPU:Celeron,1.4 GHz 等环境下,采用远程线程注入DLL的方法:(1)用GetProcAddress取得LoadLibraryA的实际地址;(2)用VirtualAllocEx在远程进程中分配一块内存(用于存放DLL的全路径名);(3)将DLL的全路径名复制到(2)所分配的内存中;(4)用CreateRemoteThread在远程进程中创建一个线程,线程函数的地址就是LoadLibraryA的实际地址,参数就是(2)中分配的内存地址,这样就把DLL注入到远程进程地址空间中。对记事本进程注入DLL进行了测试,直接法和间接法均可靠钩挂IAT中的CreateFileW函数入口地址,并实现了测试钩子函数的简单功能:(1)允许打开所选择的文件;(2)放弃打开所选择的文件。本文除对Create-FileW函数进行了钩挂测试,还对最常用的Message-BoxA、MessageBoxW和ExitProcess等导入函数用钩子模块HookPEIAT进行了钩挂测试,测试结果表明,直接法和间接法都可靠钩挂了IAT。
3 讨论
(1)替代原导入函数的钩子函数所有的参数类型应相同,参数个数要相同;返回值应相同;调用约定也应相同[2]。如果不一致可能会引发异常或不能实现钩子函数预期的目的。另外,将Win32汇编编写的钩挂MessageBoxA或MessageBoxW的DLL分别注入用C或VC++编写的程序进程中进行测试,均能可靠地钩挂IAT中的导入函数地址,测试用的钩子函数显示正常,这说明了Win32汇编写的钩子函数在调用约定,参数个数等与C或VC++是一致的。
(2)在进行钩挂前应先检查该PE文件的导入表中有无要钩挂的导入函数,如没有要钩挂的导入函数,则注入DLL后将不会有任何反映。如果钩挂前经检查PE文件的导入表中有要钩挂的函数,但注入DLL后没有反映,有一种情况是该导入函数已在DLL注入前就已执行过了,虽已钩挂但该导入函数此后没有再调用了,像这种情况可先将线程挂起后,再注入DLL,然后启动线程即可。
(3)对于用 LoadLibrary和 GetProcAddress显式装入的函数,由于PE文件的导入表中没有此函数,所以是无法通过钩挂IAT的方法钩住此函数。但可以钩挂LoadLibrary和GetProcAddress这2个函数来拦截所要钩挂的函数,实际上通过钩挂GetProcAddress就可实现拦截显式装入的函数,因为要取得函数的地址必须调用GetProcAddress。本文仅对MessageBoxA进行了简单的显式装入拦截测试。思路是:通过钩挂GetProcAddress导入函数,在钩子函数内部对每次调用GetProcAddress的每一个函数名参数都与字符串“MessageBoxA”,0进行比对,如果找到了,表明是要钩挂的函数MessageBoxA,显示拦截成功的信息,再返回GetProcAddress的值,继续调用显式装入函数MessageBoxA来显示原来的信息;如果不是字符串“MessageBoxA”,0,就直接返回 GetProcAddress的值。
(4)钩挂IAT除可采用远程注入DLL的方法外,还可以采用远程注入代码的方式;但后者需要对注入代码中的全局变量和函数进行重定位,并需要自己动态搜索API函数,而前者则没有这方面的问题。
(5)要解除对PE或DLL的IAT的钩挂,只要在钩子模块HookPEIAT中,将参数lpoldfunc(原函数入口地址)与lphookfunc(钩子函数入口地址)值进行对换,然后调用HookPEIAT,即可解除对IAT的钩挂。
(6)在实际的钩挂IAT的程序时,为了防止因考虑不周而引发异常暴露钩子函数的行为,建议编程时加入异常处理程序,以便出现小错误时异常处理程序内部自己就处理了,以保护自己的程序能隐蔽执行。
(7)对于使用了延迟加载DLL技术的PE文件,由于API函数在使用前无法在IAT中找到入口地址,这样会使直接钩挂IAT失败[1],但仍可以通过钩挂LoadLibrary和GetProcAddress来实施拦截。本文就利用钩子模块HookPEIAT,对PE文件的延迟加载的user32.dll中的MessageBoxW函数,通过钩挂GetProcAddress,成功拦截了MessageBoxW函数。
(8)对于绑定的导入表,建议在内存中钩挂导入地址表为好;因为Windows装载器在加载PE文件时会先检查所装入的DLL地址的有效性,如不满足要求,将会重新生成一个新的IAT[2],从而使静态钩挂的IAT完全失效。
4 结束语
Windows钩子技术博大精深,在防火墙、进程监控、进程隐藏、进程自我防护、实时数据采集、即时翻译、内存信息隐藏、注册表项隐藏、网络攻击与防御、反病毒、加密解密等方面得到广泛的应用,钩子技术还扩展了原函数的功能。本文所介绍的用户级IAT钩子,由于简单、可靠和适用,也得到了广泛的使用。IAT钩子在使用中有几点要注意:(1)任何阻止DLL或代码注入到进程中的方法,都会阻止钩挂IAT;还有将其注入的DLL释放掉,也会使IAT钩子失效,这可以采用注入代码的方法来应对。(2)任何对导入表或IAT进行加密处理,也会阻止钩挂IAT。对于加密的程序,只有当外壳程序把执行权交给被加密程序时,PE文件的导入表和IAT才恢复原状,这时才能正确钩挂IAT,但时间点的确定是以后钩挂IAT研究的方向。(3)反钩挂IAT。Rootkit反钩挂是通过比较IAT中的地址与DLL中导出函数的地址,如发现二者之间有任何差异[4],表明可能带有 IAT钩子,可用DLL中导出函数的地址覆盖掉IAT中钩子函数地址,使IAT钩子失效。
:
[1]任晓珲.黑客免杀攻防[M].北京:机械工业出版社,2013:375-376,390-413.
[2][美]杰夫瑞,[法]克里斯托夫.Windows核心编程(第5版)[M].葛子昂,周靖,廖敏译.北京:清华大学出版社,2008:600-601.
[3]段钢.加密与解密(第3版)[M].北京:电子工业出版社,2008:285-286.
[4][美]戴维斯,等.黑客恶意软件和RootKit安全大曝光[M].姚军,等译.北京:机械工业出版社,2011:218-219.
[5]苏雪丽,袁丁.Windows下两种API钩挂技术的研究与实现[J].计算机工程与设计,2011,32(7):2548-2552.
[6]陈云超,马兆丰.基于API函数拦截技术的跨进程攻击防护研究[C]//2011年通信与信息技术新进展——第八届中国通信学会学术年会论文集.2011.
[7]舒敬荣,朱安国,齐善明.HOOK API时代码注入方法和函数重定向技术研究[J].计算机应用与软件,2009,26(5):107-110.
[8]黄顶源,李陶深,严毅,等.HOOK API在内网安全监管系统中的应用[J].广西物理,2006,27(4):38-41.
[9]陶廷页,孙乐昌,汪永益.利用API HOOK技术实现计算机保密通信[J].安徽电子信息职业技术学院学报,2004,3(5-6):107-108.
[10]邓乐,李晓勇.基于IAT表的木马自启动技术[J].信息安全与通信保密,2007(2):151-153.
[11]王泰格,邵玉如,杨翌.全局IAT Hook技术原理及实现[J].信息与电脑:理论版,2012(7):110-111.
[12]程彦,杨建召.Win32中API拦截技术及其应用[J].长春工业大学学报:自然科学版,2006,27(4):369-371.