您的位置:首页 > 其它

揭示Win32 API拦截细节/API hooking revealed (2)

2013-03-29 13:56 239 查看
2.Win32用户级钩子

A. 窗口子类

这种方法适用于那些会根据不同窗口过程的实现而具有不同行为的应用程序。要完成上述工作(通过更改窗口过程来执行用户自定义代码),只需对该特定窗口简单调用SetWindowLongPtr(),传递GWLP_WNDPROC和用户自定义窗口过程的指针作为实参即可。一旦建立好用户自定义窗口过程,以后windows每次分发消息到目标窗口时,都会调用用户自定义的窗口过程了。

这种机制的缺点是,子类只在指定进程(当前进程)边界范围内(the boundaries of a specific process)有效。就是说,应用程序不能为其它进程创建的窗口建立窗口子类。

通常,这种方法适用于通过插件拦截应用程序,这样就能够取得要替换窗口过程的窗口的句柄了。

例如,以前我写过一个简单的IE插件(BHO),它通过窗口子类把IE的浮动菜单替换掉。

B. 代理DLL(DLL木马)

拦截API的另一种简单方法是,用具有相同名称、相同导出符号的DLL替换掉应用程序原来的DLL。借助函数导出节(function forwarders)实现这种技术会很容易。从根本上说,函数导出节就是DLL入口处的导出节,它代表本模块与其它DLL的函数调用关系。

你可以简单使用#pragma comment完成以上工作:

#pragma comment(linker, "/export:DoSomething=DllImpl.ActuallyDoSomething")

如果你决定使用这种方法,你应该自行处理库新旧版本之间的兼容性问题。更多信息请参考[参考13a]“Export forwarding”和[参考2]“Function Forwarders”。
C. 代码重写(Code Overwriting)

很多函数拦截的方法都基于代码重写。其中一种通过改变call指令的目标地址实现代码重写。这种方法使用困难,而且容易出错。基本思想是,拦截内存中所有的call指令并以用户提供的地址替换其原来的函数地址。

代码重写的另一种方法实现起来更复杂。简单说,基本思想就是先定位原有API函数地址,然后通过jmp指令改变函数体前几个字节来重定向到用户自定义的API函数去执行。这种方法需要极强技巧性,并涉及到对每个call调用的一系列恢复和拦截操作。要指出的一点是,如果函数处于未被拦截的状态(unhooked mode)并且该函数正被调用,则将不能拦截到对该函数的下一次调用。

上述方法的主要问题是,它跟多线程环境中的线程规则相冲突。

尽管如此,还是有巧妙解决方法的,它解决了一些问题并提供了可以基本实现API拦截的成熟的方法。如果对上述问题有兴趣,可以查看[参考12]的Detours解决方案。
D.通过调试器拦截api调用

另一个替代的方法是在目标函数内插入断点。但这种方法也有些缺点。主要问题是抛出调试异常时(debugging exceptions)会挂起当前应用程序所有子线程的执行。还需要一个调试线程处理这个异常。另一个问题是,当调试过程(debugger)完成时,windows就会把调试器关闭。

E.通过改变导入地址表拦截api调用

这种技术最初由 Matt Pietrek 公布,后来由 Jeffrey Ritcher ([参考2] “通过操作模块的导入节实现API拦截”)和 John Robbins ([参考4]“拦截导入函数”)加以详细描述。这是种强大简单而且容易实现的方法,也满足在winNT/2k和9x上运行拦截系统的大部分需求。这种技术基于windows的可执行文件结构。要理解这种方法的工作过程,必须熟悉PE文件结构,它是通用文件对象格式(COFF)的扩展。Matt Pietrek 在[参考6]“深入PE格式”以及[参考13a/b]“win32PE格式深度透视”中详述了PE格式的相关细节。我将给出PE格式的概述,旨在让读者明白通过操作导入表实现api拦截的思想。

通常来说,一个PE二进制文件的格式是经过组织的,因此它具有代码节(code sections)和数据节(data sections),这与可执行文件在内存中的格式是一致的。PE文件格式在逻辑上由几个节组成,每个节维护特定的数据,并符合操作系统程序加载器的特定要求。

请注意 .idata节,它包含导入地址表的信息。这部分信息对于一个更改IAT拦截api调用的系统相当重要。

符合PE格式的可执行文件都具有下图所述的结构:




图3

应用程序加载器负责把应用程序以及与它相链接的DLL加载到内存,由于这些DLL被加载到的内存地址是不可预料的,因此加载器不能断定各个导入函数的实际地址。加载器必须做一些额外工作来保证应用程序能成功调用每个导入函数。但是对于内存中的每个可执行文件映像,逐个修改它们的每个导入函数会花费大量的处理器时间并导致性能下降。那么,加载器是如何处理这个问题的呢?关键在于,对同一个导入函数的每次调用都指向相同地址,那就是函数代码驻留在内存的位置。实际上对导入函数的调用都是间接调用,即通过一条间接JMP指令并结合IAT实现寻址。这样的好处是加载器不需要扫描整个可执行映像。这种方法看上去很简单,它仅仅是改变IAT内导入函数的地址。这里是一个简单win32程序的PE结构的示例,并借助了[参考8]的PEView 工具。可以看到TestApp导入表包含了两个由GDI32.DLL导出的函数:TextOutA() 和 GetStockObject()。





图4

实际上拦截一个导入函数并没有那么难。总之,一个通过修改IAT来拦截api的拦截系统需要找出IAT中存储的导入函数地址,并用自定义函数的地址覆盖它。这个用户自定义的函数必须和被替换函数的原形一致,这点很重要。下面是替换的步骤:

1、对于目标进程以及它加载的每个DLL,都要通过IAT定位导入节的位置。

2、找到导出目标函数的DLL的IMAGE_IMPORT_DESCRIPTOR 束。实际上,我们通过DLL的名称来找到这个入口。

3、找到含有目标函数地址的IMAGE_THUNK_DATA 束。

4、用自定义函数的地址替换原有地址。

要改变IAT内导入函数的地址,我们必须保证所有对目标函数的调用都被重定向(re-routed)到钩子函数。

还有一点是,需要改写的 .idata 节不一定都是可写的,这就要求我们保证 .idata 节可写。可以调用 VirtualProtect() 来实现。

另一点值得注意的是,GetProcAddr() 函数在win9x系统上的行为。当一个程序在非调试模式调用这个API,它会返回目标函数的指针;但如果在调试时调用,它会返回一个与上述指针不同的地址。这是由于在调试的时候,GetProcAddr() 返回的是指向目标函数指针的指针。这个指针指向一条带有目标函数地址的PUSH指令。就是说,在win9x系统上迭代(IMAGE_THUNK_DATA)束时,我们必须检查这个函数指针是否带有PUSH指令(在x86平台上是0x86),并相应地获取函数的实际地址。

Win9x并不支持写时拷贝,因此操作系统会尝试阻止调试器访问2GB边界以上的函数。这就是GetProcAddr() 返回调试指针(debug thunk)而不是实际地址的原因。John Robbins 在[参考4]“拦截导入函数”中讨论了这个问题。
注入拦截DLL的时机

在此前已经讨论过,选择的注入机制并不是操作系统本身的固有机制时开发人员所面临的困难。例如,当使用内置的windows钩子注入DLL时,注入机制就不是开发人员所要关心的问题。强制每个符合要求的进程加载DLL[参考18],这是操作系统需要完成的工作。事实上,windows跟踪所有新建立的进程并强制它们加载钩子DLL。通过注册表来管理dll注入与windows 钩子类似。这些方法的最大的好处是它们本身就是操作系统的一部分。

与上述注入机制不同的是,利用CreateRemoteThread() 的注入方法还要求维护当前运行的进程列表。如果没有及时注入,拦截系统将会丢失一些原本需要拦截的api调用。每当一个新进程开始或关闭时,钩子服务器(Hook Server)都要使用一种巧妙的方法来接收相关的通知,这很重要。其中一种方法,是通过拦截并监视CreateProcess() 系列的API函数的调用(来获得进程开启或关闭的通知)。这时当用户自定义函数被调用,它就可以通过添加Create_SUSPENDED标志调用原来的CreateProcess()。这意味着目标进程的主线程将被挂起,同时钩子服务器将有机会利用手写的机器码指令把DLL注入到目标进程,然后使用ResumeThread() 唤醒目标进程。细节可以参考[参考2]“利用CreateProcess()远程注入代码”。

另一种检测进程执行的方法,是基于驱动程序的。值得关注的是它极高的灵活性。Windows nt/2k 提供了一个由NTOSKRNL导出的名为 PsSetCreateProcessNotifyRoutine() 的函数,这个函数允许增加一个回调函数,每当有进程产生或终止时这个回调函数都被调用。更多细节请参见[参考11]和[参考15]。

枚举进程和模块

有时候我们希望利用CreateRemoteThread() API来注入DLL,特别是当拦截系统运行在windows NT/2K上。这种情况下,钩子服务器启动后都会枚举所有活动进程并把DLL注入它们的地址空间。Windows 9x和windows2k 都内置了这种进程枚举机制(Tool Help 库,辅助库)的实现(由Kernel32.dll实现)。另一方面,windowsNT使用PSAPI库达到相同目的。于是,我们需要一种能够令钩子服务器正确运行并动态决定当前可用“帮助库”的途径。因此,拦截系统被设计成可以判断当前操作系统支持何种帮助库,并相应采用合适的API。

我将要展示一个以面向对象为基础的简单框架,它用于在windows9x以上操作系统枚举进程和模块。我的设计允许用户根据自己的需求来扩展框 架的功能。框架的实现也是相当简单明了的。

CTaskManager类实现了整个枚举子系统的处理核心,它负责生成一个进程枚举库对象(例如CPsapiHandler 或 CToolhelpHandler)来调用正确的进程信息提供库(例如在9x和2k上分别是psapi和toolHelp32)。CTaskManager也负责产生和维护一个记录当前所有进程列表的容器对象。在CTaskManager实例化之后,拦截系统调用Populate()函数。这个函数强制性地枚举系统中所有进程和dll库并把它们的信息保存在CTaskManager的成员m_pProcesses中。

下面的uml图展示了上述子系统各个类之间的关系:





图5

此处要着重指出的一条是,事实上,windowsNT的Kernel32.dll并没有实现任何的ToolHelp32函数。因此我们必须使用运行时动态链接的方式额外链接这些函数。在windowsNT上如果使用静态链接,那么不论应用程序是否曾经试图调用任何ToolHelp32函数,代码都会运行失败。更多信息请参见我的文章“在windows9x/2k和windowsNT上枚举进程和模块的单一接口”。

建立钩子工具系统(Hook Tool System)的必要条件 目前我已经对拦截过程中用到的各种原理概念作了简短介绍,现在是时候来确定建立一个拦截系统的必要条件,并研究其详细设计了。下面是有关这个系统的一些总结:

1)提供用户级拦截系统,对通过名字导入的win32 api函数实施拦截

2)提供一种方法,可以用windows钩子 或远程线程把拦截驱动注入到所有正在运行的进程。拦截系统应该提供ini文件来选择使用何种方式

3)使用一种基于更改iat的拦截方法

4)拦截系统的架构是基于面向对象技术的、可重用的、可扩展的和分层的

5)用一种高效的可扩展的机制来拦截api函数

6)(整个系统)能够达到预期的性能要求

7)在拦截驱动和钩子服务器之间使用一种可靠的数据传输机制

8)实现TextOutA()、TextOutW()以及ExitProcess()的拦截

9)把拦截过程发生的事件都记录在日志中

10)拦截系统可以运行在任何基于Intel x86架构的windows9x或以上的操作系统

设计和实现

本部分将讨论钩子架构的关键部分以及它们之间的通讯方式。这样的架构能够拦截任何通过名称导入的api函数。

在概述拦截系统的设计之前,请留意几种注入和拦截方式。

首先,要选择一种能够把dll注入系统所有进程的方式。因此我设计了一个抽象基类,并实现两种注入技术,根据ini文件的设置和操作系统版本(例如windowsNT/2k或windows9x)来选择两种注入方法中的一种。这两种方法分别是全局windows钩子和远程线程。在示例代码中,同时使用了windows钩子机制和远程线程的方式在windowsNT/2k系统上注入dll。可以通过修改一个包含拦截系统所有设置的ini文件来选择注入方式。

另一个重点是选择拦截机制,不必惊奇,我将使用修改iat来作为拦截win32api的最有效方法。为了达到预期的效果,我设计的钩子框架将包含如下组件和文件:

1)TestApp.exe – 一个用于测试的简单win32应用程序,它仅仅使用TextOut()简单的输出了一行文本。它的目的只为了展示api的拦截过程。

2)HookSvr.exe - 控制dll注入和钩子安装的程序。

3)HookTool.dll –用win32 dll方式实现的钩子库

4)HookTool.ini – 配置文件

5)NTProcDrv.sys – 一个小型的用于监视进程产生和撤销的windowsNT/2k内核模式驱动。这个组件是可选的,它仅用于在windowsNT以上系统监视进程。

HookSrv是一个简单的控制程序。它主要用来加载HookTool.dll并激活拦截引擎。加载dll以后,钩子服务器传递一个隐藏窗口的句柄同时调用InstallHook(),HookTool.dll把所有消息都发送到这个窗口上。HookTool.dll实现了拦截驱动且还是拦截系统的核心。它实现了真正的拦截操作并拦截了TextOutA()、TextOutW() 和 ExitProcess()。

尽管文章着眼于windows内部机制因而没必要使用面向对象方法,但我仍使用可重用c++类封装了相关操作。这样可以提供更多的灵活性并使系统易于被扩展。开发者也可以在本工程外的其它工程使用其中独立的类,这对他们有好处。

下面的UML图解释了HookTool dll所实现的各个类之间的关系。




图6
此处请大家留意HookTool.dll的类架构。其中设计各个类的功能是开发过程的重要一环。每个类实现一个特定功能并对外表现为一个独立的逻辑整体。

CmoduleScope是整个系统的基类。它用Singleton模式实现且是线程安全(thread-safe)的。它的构造函数接受3个在共享数据段声明的指针,这些指针将被所有进程共享。基于上述方法,在类中这些变量可以很容易被维护,而不会破坏类封装的原则。

当一个应用程序加载HookTool库时,dll在接收到DLL_PROCESS_ATTACH消息后便产生一个CModuleScope实例。这一步初始化了CmoduleScope的唯一实例。CmoduleScope对象构造的重要一环是产生一个合适的dll注入器对象。而选择合适的注入器是在解释HookTool.ini文件并判断[Scope]节下的UseWindowsHook参数后发生的。当拦截系统运行在windows9x时,这个参数的值将不会被解释,因为windows9x不支持远程线程的注入方式。

上述实例化步骤完成后,接着就会调用ManageModuleEnlistment() 。以下是该函数的一个简化版本:
[align=left][cpp] view plaincopy[/align]

[align=left]1. // Called on DLL_PROCESS_ATTACH DLL notification [/align]
[align=left]2. BOOL CModuleScope::ManageModuleEnlistment() [/align]
[align=left]3. ......{ [/align]
[align=left]4. BOOL bResult = FALSE; [/align]
[align=left]5. // Check if it is the hook server [/align]
[align=left]6. if (FALSE == *m_pbHookInstalled) [/align]
[align=left]7. ......{ [/align]
[align=left]8. // Set the flag, thus we will know that the server has been installed [/align]
[align=left]9. *m_pbHookInstalled = TRUE; [/align]
[align=left]10. // and return success error code [/align]
[align=left]11. bResult = TRUE; [/align]
[align=left]12. } [/align]
[align=left]13. // and any other process should be examined whether it should be [/align]
[align=left]14. // hooked up by the DLL [/align]
[align=left]15. else [/align]
[align=left]16. ......{ [/align]
[align=left]17. bResult = m_pInjector->IsProcessForHooking(m_szProcessName); [/align]
[align=left]18. if (bResult) [/align]
[align=left]19. InitializeHookManagement(); [/align]
[align=left]20. } [/align]
[align=left]21. return bResult; [/align]
[align=left]22.} [/align]

ManageModuleEnlistment() 的实现简单明了,通过检查m_pbHookInstalled指向的变量,它测试自身是否已经被钩子服务器调用过。如果已经被调用,它就只是简单的把sg_bHookInstalled设为TRUE,表明钩子服务器已经启动了。

接着,钩子服务器通过调用钩子dll的导出函数InstallHook() 来激活钩子安装引擎。实际上,该函数只是简单调用了CmoduleScope的InstallHookMethod() 函数。这个函数的作用是强制目标进程加载或卸载HookTool.dll。HookTool.dll提供了两种把自身注入外部进程空间的方法——一是使用Windows钩子另外一种利用CreateRemoteThread()函数。在该系统的架构上,定义了一个抽象类CInjector以及用于注入和卸载dll的纯虚函数。类CWinHookInjector和CremThreadInjector都从CInjector继承。尽管如此,但它们提供了两个纯虚函数InjectModuleIntoAllProcesses()和EjectModuleFromAllProcesses() 的不同实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: