第6章APIHOOK
- 格式:docx
- 大小:382.42 KB
- 文档页数:30
第6章API HOOKAPI HOOK始终是系统程序员关心的热门话题。
通过API HOOK,可以对Win32 API函数进行拦截,从而改变函数乃至程序的固有行为。
通过API HOOK拦截可以解决十分棘手的难题。
本章主要介绍Win32API拦截、驱动程序服务函数的拦截、BrowserHelpObject实现、SPI接口的应用,并利用Detours库实现对DLL的绑定。
6.1 API HOOK综述API HOOK尽管能够提供非常强大的功能,但是遗憾的是,无论是DDK还是SDK都没有提供这方面的任何文档和例子。
使用API HOOK前,用户首先应该弄清楚,究竟是拦截一个应用程序还是系统范围内的拦截。
系统的拦截往往需要实现对创建的新的进程进行监视。
对系统底层服务进行拦截,能够打破进程边界。
最简单的实现拦截的方法是实现一个代理的动态链接库。
这个动态链接库实现了目标拦截动态链接库的全部输出函数,但是它只是一个函数转发器,其函数功能还是通过间接地调用原来的目标动态链接库函数实现。
当程序或者系统调用目标动态链接库的时候,真正调用的是代理动态链接库。
代理动态链接库的输出函数会首先检查调用函数的入口参数,决定直接返回一个值,还是调用原来的函数。
可以把这个函数的返回结果作为返回值返回,也可以对调用结果进行二次处理。
由于这种方法需要对所有的函数进行处理,这个过程十分繁琐,而且并非所有的函数都提供了足够的文档,因此这种方法在某些情况下几乎是无法实现的。
这方面应用的例子有关于WinInet实现的程序,发表在1994年12月份的MSJ杂志上,网址是/MSJ/0997/hood0997.htm。
对API函数的拦截不外乎两种方式,要么在磁盘文件上实现,要么在内存中实现。
前者拦截在可执行程序加载以前,后者则是在程序加载之后。
两种方法基本上都是通过打补丁的方法实现的。
如果用户以前曾经研究过API HOOK,也许听说过导入地址表补丁的概念。
它的很多优点使得这种Windows API HOOK的方法成为最体面、最常见的方法之一。
这种方法的基础主要是基于这样的事实,即所有的32位Windows可执行文件或者动态链接库都是基于PE(PortableExecutable)文件格式构建的。
这种方法构建的文件由多个块组成,这些块称为节。
每个节中都包含了特定的内容。
比如说,.text节保存了可执行应用程序的代码,而.rsrc节则是为了保存程序中使用的各种资源,比如对话框、菜单、位图、工具栏等。
在Windows可执行文件的所有节中,.idata节对于实现API拦截尤其有用。
在这个节中包含了一个导入地址表,它保存着所有与可执行代码引用的导入函数名称的偏移量。
当Windows加载一个可执行文件到内存中的时候,它就会使用正确的导入函数地址来修正这个偏移量。
也许用户会问,Windows为什么会不厌其烦地去修正导入地址表呢?这是因为所有的可执行文件和动态链接库在加载到内存中时都需要重新定位。
如果在可执行代码中预先规定好导入函数的目标调用地址,这往往是不可能的。
为了确保这些调用能够成功地连接到它们的目标,Windows很有必要在可执行文件映像加载到内存中时,对导入函数的每一个调用进行修补。
很明显,这个初始化的过程是一个耗时的过程,加载的可执行文件依赖的动态链接库越多,这一点表现的就越明显。
如果用户的程序加载时间格外长,那么应该考虑把那些不必要的隐式加载移动到动态加载中来。
Windows的设计者并非那么鼠目寸光,他们在地址问题上表现得特别聪明。
在当前的Windows可执行文件或者动态链接库实现中,所有对导函数的调用都是借助于导入地址表通过一个间接的跳转(JMP)实现的。
很明显,Windows导入地址表这种重新定向的机制为API函数的拦截提供了方便。
为了实现拦截,用户可以用自己函数的入口地址覆盖这个导入地址表项,这样函数调用时,首先加载的应该是用户提供的函数。
另外一种办法是对函数的实现进行修补。
这种方法主要是通过替换函数实现的前几个字节实现的,每一个函数实现时,无非一对参数入栈指令,可以把这些指令的前几个字节保存到一个地址中,然后用一个跳转指令替换这些字节。
当目标函数被调用的时候,首先执行的是跳转指令,直接把控制交给了用户提供的替换函数。
下面的图6-1是这种实现的机理。
图6-1在上面的图6-1中,函数拦截时,在函数的入口地址处放置了一个Jmp指令,把控制权直接转移到了替换函数LoggingFunction。
而在LoggingFunction函数中如果需要实现原来函数的功能,则可以通过callStub指令实现。
而Stub函数地址则以原来保存的几个字节和一个跳转指令组成。
很明显这种方法对拦截的函数有一定的限制,即这个函数入口地址处代码不能少于5个字节。
当然这也不是什么条件,一般情况下,使用5个字节是很难实现一个函数的,即便勉强实现了,拦截这么一个几乎没有任何功能的函数没有任何意义。
这种方法实现的典型代表是微软提供的Detours,一个通用的API函数拦截库。
解决了函数拦截问题,问题还没有结束,用户还需要考虑进程边界的问题,即如何让自己的拦截代码运行在正确的地址空间。
在回答这个问题之前,用户最好对Windows内存管理的架构有所了解。
前面提过,所有的32位Windows应用程序都运行在一个相对独立的私有地址空间。
任务切换时,Windows会更新页表,把一个新进程的线性地址空间映射到物理内存中,即进程内存环境。
这样除了共享内存区域保持不变外,原来进程的内容就会被一个新进程映射完全刷新,再也找不到它的踪影。
在Windows9x环境下,4GB的线性地址空间被划分成几个不同的内存区域,这些区域都是为原来预定的目的保留的。
MS-DOS和16位的全局堆的前面一部分占用了最底部的4MB空间,紧挨着的区域(4MB~2GB)用于Windows9x加载每一个进程代码、数据和动态链接库。
由于每一个进程的物理地址都是惟一的,因此Windows9x必须确保当一个指定的进程被激活时,整个页面表对应(4MB~2GB)的被映射到该进程的物理内存页。
这种切换机制造就了所有进程共享(4MB~2GB)同一线性内存区域,但不是相同的物理地址。
这就好像这个区域是该物理地址空间的一个窗口,这个窗口能够根据当前进程的不同显示它对应的不同视图。
总之,进程内存的表现形式只是它磁盘映像的一个副本。
从2GB到3GB的内存区域,归16位全局堆高位部分所有,同时这个区域还用于实现内存共享文件,寄存Windows使用的系统动态链接库(比如USER32、GDI32和KERNEL32),这些内容能够被所有正在运行的进程共享。
这个区域对于API函数拦截来说,尤其有用,因为它对于所有活动的地址空间而言都是可见的,这里面的数据和代码能够被所有的进程来存取。
对于WindowsNT/2000,内存分布情况大不一样,很少有文档披露一种方法能够把动态链接库加载到所有进程都能够共享的区域。
惟一的一种方法是要求目标进程能够把监视的动态链接库注入到它所在的地址空间。
最简单而且最不常用的方法是通过注册表实现:HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_Dlls。
这个键会导致Windows把动态链接库加载到系统中每一个进程的地址空间。
不幸的是,这种技术只适用于那些链接到User32.Dll的进程,如果是一个控制台应用程序,没有使用User32中实现的输出函数,这种方法将不起作用。
关于打破进程边界,实现代码注入的方法在前面的第3章已经谈到很多,钩子函数、创建远程线程等都可以实现动态链接库代码的注入。
另外在什么时间注入也是一个值得考究的问题,错误的注入时间会错失拦截的良机,特别是对于系统范围内拦截的情况。
最好的完成时机应该是监视对CreateProcess函数的调用。
在替换CreateProcess函数实现时,可以使用CREATE_SUSPENDED作为创建标志替换调用,这样能够保证进程启动时处于挂起状态。
在挂起阶段,拦截程序可以完成动态链接库注入,然后使用ResumeThread函数重新激活线程运行。
当然还有其他方法能够实现进程的执行,但是它们都是异步实现的,这些方法并不适合用于判断何时注入动态链接库。
关于进程创建监视的代码前面第3章已经作了非常详尽的介绍。
比如,在Windows9x环境下,当一个新的进程创建的时候,VWin32会发送一个CREATE_PROCESS消息,可以在一个虚拟设备驱动程序中拦截这个消息,然后通知给Win32应用程序。
对于WindowsNT/2000可以在WDM驱动程序中使用PsSetCreateProcessNotifyRoutine函数创建一个进程创建的回调函数。
在众多的API HOOK应用中,用户最关心的莫过于对网络活动的检测了,这些程序包括防病毒程序、个人防火墙和Internet基于内容过滤的程序。
由于大部分的网络程序都是基于Winsock函数实现的,所以对Socket函数的拦截,备受用户的青睐。
对Socket函数的拦截并不需要实现一个完整的Winsock函数集。
Winsock2提供了一种LSP(UnravelingtheMysteriesofWritingaWinsock2LayeredServiceProvider)机制,这种机制允许用户以链状的方式挂接上一层的Socket函数。
这种挂接的机制和上面提到的API HOOK方法有着很大的不同,但是实现替换函数的方法却有着异曲同工之妙。
用户可以访问/msj/defaultframe.asp?page=/msj/0599/LayeredService/LayeredService.htm链接获得这方面的信息。
另外一种监视网络活动的方法是编写上层NDIS驱动程序,通过编写中间层的驱动程序或者挂接NDIS 接口,不仅可以监视TCP/IP通信,还可以监视通过网卡的任何其他数据。
NuMega的VToolsD(/drivercentral/components/vtoolsd.shtml)提供了一个HookTDI的例子程序,微软也提供了一个NDis中间驱动的例子程序(/support/DDK/Ntddk/NTSamples/default.asp)。
针对浏览器访问的链接的跟踪,通过API拦截往往效率不高。
这时用户可以使用InternetExplorer4.0以上版本提供的BrowserHelperObjects(/Mind/0598/browhelp.htm)。
对于NetscapeNavigator浏览器,用户可以采用DDE机制。