拦截win32 API 调用(下)

拦截win32 API 调用(下)

设计和实现
     本部分将讨论钩子架构的关键部分以及它们之间的通讯方式。这样的架构能够拦截任何通过名称导入的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() 。以下是该函数的一个简化版本:

// Called on DLL_PROCESS_ATTACH DLL notification

BOOL CModuleScope::ManageModuleEnlistment()

......{

           BOOL bResult = FALSE;

           // Check if it is the hook server

           if (FALSE == *m_pbHookInstalled)

           ......{

                     // Set the flag, thus we will know that the server has been installed

                     *m_pbHookInstalled = TRUE;

                     // and return success error code

                     bResult = TRUE;

           }

           // and any other process should be examined whether it should be

           // hooked up by the DLL

           else

           ......{

                     bResult = m_pInjector->IsProcessForHooking(m_szProcessName);

                     if (bResult)

                                InitializeHookManagement();

           }

           return bResult;

}

     ManageModuleEnlistment() 的实现简单明了,通过检查m_pbHookInstalled指向的变量,它测试自身是否已经被钩子服务器调用过。如果已经被调用,它就只是简单的把sg_bHookInstalled设为TRUE,表明钩子服务器已经启动了。
     接着,钩子服务器通过调用钩子dll的导出函数InstallHook() 来激活钩子安装引擎。实际上,该函数只是简单调用了CmoduleScope的InstallHookMethod() 函数。这个函数的作用是强制目标进程加载或卸载HookTool.dll。HookTool.dll提供了两种把自身注入外部进程空间的方法——一是使用Windows钩子另外一种利用CreateRemoteThread()函数。在该系统的架构上,定义了一个抽象类CInjector以及用于注入和卸载dll的纯虚函数。类CWinHookInjector和CremThreadInjector都从CInjector继承。尽管如此,但它们提供了两个纯虚函数InjectModuleIntoAllProcesses()和EjectModuleFromAllProcesses() 的不同实现。

// Activate/Deactivate hooking engine

BOOL    CModuleScope::InstallHookMethod(BOOL bActivate, HWND hWndServer)
...{
           BOOL bResult;
           if (bActivate)
           ...{
                     *m_phwndServer = hWndServer;
                     bResult = m_pInjector->InjectModuleIntoAllProcesses();
           }
           else
           ...{
                     m_pInjector->EjectModuleFromAllProcesses();
                     *m_phwndServer = NULL;
                     bResult = TRUE;
           }
           return bResult;
}
         
     CwinHookInjector类用Windows钩子实现注入机制。它通过如下调用安装过滤函数(Filter Function):正如你在上面看到的,它向系统注册了WM_GETMESSAGE类型的钩子。钩子服务器只调用这个函数一次。SetWindowsHookEx() 的最后一个参数是0,因为在这里GetMsgProc() 是作为一个全局钩子来使用的。当某个窗口即将接收一条特定消息时,回调函数就被调用。这里有趣的是,我们仅为回调函数GetMsgProc() 提供了几近空的(什么也不做的)实现,因为我们不需要监控窗口的消息处理。我们提供该函数的实现仅仅是要用到操作系统提供的注入机制。
// Inject the DLL into all running processes
BOOL CWinHookInjector::InjectModuleIntoAllProcesses()
...{
           *sm_pHook = ::SetWindowsHookEx(
                     WH_GETMESSAGE,
                     (HOOKPROC)(GetMsgProc),
                     ModuleFromAddress(GetMsgProc),
                     0
                     );
           return (NULL != *sm_pHook);
}

     调用SetWindowsHookEx()后,操作系统将检查导出GetMsgProc()的dll(例如HookTool.dll)是否已经被映射到GUI进程的地址空间。如果未被加载,Windows将强制该进程映射它。有趣的是,全局钩子的DllMain()函数不应该返回FALSE。这是因为windows会不断验证DllMain()的返回值并加载该dll直到DllMain()返回TRUE。
     另一个完全不同的实现来自CRemThreadInjector类。此处的注入机制基于产生远程线程。CRemThreadInjector通过接收进程产生和终止的消息来扩展Windows的进程列表维护机制。也就是产生一个CNtInjectorThread对象来监视进程产生。CntInjectorThread从内核驱动接收进程产生的相关消息。因此每次有进程产生,CNtInjectorThread ::OnCreateProcess() 都被调用,相应的,当进程终止将自动调用CNtInjectorThread ::OnTerminateProcess()。与windows钩子不同的是,每次有进程产生的时候,采用远程线程机制都要手动向新线程注入dll。这需要我们提供一种发送进程产生事件的简便方法。CntDriverController类实现了一些列管理服务和驱动的api。它被设计成加载和卸载内核模式驱动NTProcDrv.sys。后面将会讨论它的实现。
     成功向特定进程注入HookTool.dll后,该dll的DllMain()将调用ManageModuleEnlistment()。在前面我已经阐述过了重复调用该函数的原因。它通过CmoduleScope的成员m_pbHookInstalled检查共享变量sg_bHookInstalled。由于钩子服务器在初始化时已经把sg_bHookInstalled置TRUE,于是它便检查当前进程是否需要被拦截,如果需要,钩子服务器将为该进程激活拦截引擎。
     在CmoduleScope ::InitializeHookManagement()的实现中,拦截引擎被激活。这个函数用于拦截LoadLibrary()系列的函数和GetProcAddress() 函数。用这种方法,我们可以在进程初始化后监视dll的加载。每当一个新的钩子dll被映射到目标进程,它都要修改进程的导入地址表,因此我们可以保证系统不会丢失对目标函数的任何调用。
     在CmoduleScope ::InitializeHookManagement() 末尾,我们对目标函数的钩子函数作了初始化。
     由于示例代码拦截了多于一个的用户提供的win32 api函数,因此我们应该对每个被拦截的函数提供独立的钩子函数。就是说,用上述更改iat拦截api的方法,不仅需要把原目标函数在导入表中的地址更改为一个“通用的”钩子函数的地址。拦截系统需要知道某个函数调用指向哪个函数。另外一点很重要的是,钩子函数必须有跟原api函数相同的函数原型,否则堆栈将会崩溃。例如CModuleScope实现了MyTextOutA()、MyTextOutW()和MyExitProcess()3个静态函数。一旦HookTool.dll被加载到目标进程,并且拦截系统被激活,每次对原TextOutA()的调用都回变成对CmoduleScope ::MyTextOutA()的调用。
     前面提及的拦截系统是非常灵活高效的。尽管如此,该系统一般只适合于拦截那些名称和原型已知且数量有限的api函数。
     如果你需要拦截新的函数,你应该简单声明并实现它的钩子函数,就如同前面实现MyTextOutA/w()和MyExitProcess()一样。然后你应该用前面在InitializeHookManagement()中实现的方法注册被拦截的函数。
     拦截和跟踪进程产生对实现那些需要处理外部进程的系统十分有用。获取进程产生过程中的有用信息向来是开发进程监视系统和拦截系统过程中的一个经典问题。Win32 api提供了一系列的库([参考16]PSAPI和ToolHelp库)用来枚举当前系统中运行的进程。尽管它们功能很强大,却没有提供接收进程产生/撤销事件的方法。幸运的是,windowsNT/2K提供了一系列api,被详细记录在windowsDDK文档的“进程结构规则(Process Structure Routines)”下,并被NTOSKRNL导出。其中之一的PsSetCreateProcessNotifyRoutine()允许注册一个全局回调函数,该函数在进程产生、退出、终止时被操作系统调用。通过写一个NT内核模式驱动和win32用户模式控制程序,就可以简单的利用上述函数来实现进程监视。该驱动的作用是检测进程产生并把相关事件通知控制程序。示例代码中的windows进程监视器NTProcDrv提供了在基于NT内核的操作系统上监视进程的最基本功能。更多细节请参见文章[参考11]和[参考15]。相关代码在NTProcDrv.c中。由于该驱动的加载和释放是动态的,因此当前登录用户必须拥有管理员权限。否则驱动安装将会失败同时进程监视也会出错。解决办法是用管理员权限手动安装驱动,或使用windows2k提供的“作为其他用户运行(run as different user)”选项来运行HookSrv.exe。

     最后要注意的一点是,可以通过简单修改ini文件(HookTool..ini)中的相关设置来选用上述所有的工具。这个文件决定了使用windows钩子(在win9x和nt/2k系统上)还是远程线程(仅在windowsnt/2k上)来做dll注入。也可以在该文件里面指定要监视的进程和不被监视的进程。如果需要查看被拦截进程的详细活动,打开[Trace]节下面的Enabled选项就可以记录下拦截系统的所有活动。通过调用由ClogFile类导出的方法,这个选项能让用户看到发生错误的内容。实际上,ClogFile提供了线程安全的实现,而且访问共享资源的同步问题也不需要操心了(日志文件)。更多信息请参看ClogFile类的实现以及HookTool.ini的内容。

示例源代码
     编译本工程需要vc++6 sp4和Platform SDK。在windowsNT平台上需要提供PSAPI.DLL以运行CtaskManager的实现。在运行示例代码之前必须确保已经被设置好了HookTool.ini文件。对于那些要扩展NTProcDrv代码和对本工程底层内容感兴趣的开发人员,他们应该安装windowsDDK。

延伸内容
      出于简化问题的目的,在本文中我忽略了一些内容:
          1)监视内部的api(Native API)调用
          2)windows9x系统上监视进程产生的驱动
          3)支持UNICODE,但你还是可以通过拦截UNICODE导入函数来达到相同目的。

总结
     到目前为止,本文尚未对拦截任意api的问题给出完整的指南,毫无疑问,本文遗漏了一些细节。尽管如此,我仍在这少数的有限篇幅中给出了足够重要的信息,也许对那些在win32用户模式做api拦截的开发人员有帮助。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值