windows API 勾子内幕初探

这篇技术文章英文原文来源于www.codeguru.com,

经过本人翻译和修改

术语约定:

hook/hooking:勾子,勾挂,勾子技术,监控

进程标志:process token

spying system :监控系统或监视系统

Win32 subsystem API: Win32子系统API

Native API:本地API

Intercept:干预/干涉

Import Address Table:地址导入表,或直接称IAT

其他不太明确的专业术语在括号内标有原文,其中如果是括号里的解释性的附加文字注有“译注:”

正文:

    WIN32调用拦截是大多数windows程序员的一个挑战主题,我得承认这也是我最喜欢的话题之一。勾子技术描述的是一种监控正在执行的特定代码的技术。它提供了这样一种直截了当的机制,通过这种机制能够轻松的以第三方的身份更改(源程序制定的)操作系统的行为,而不需要知道程序源代码。

   有很多新方法描述了他们通过监控技术去利用现有Windows应用程序的意图。使用勾子技术的主要动机,不仅仅是为了去实现更高级的功能,也有想达到注入用户代码进行程序调试的意图。

   和一些相关的如DOS和Windows 3.xx等“老”操作系统不同,当今的NT/2K和9x等windows操作系统提供了复杂的机制去分离每个进程的空间寻址.这种机制提供了内存保护,因此任何一个应该程序不可以去干预另一个进程的地址空间或者甚至企图使系统本身崩溃。这使得系统“勾子”技术更加困难。

   我写这篇文章的目的是为了阐述一个简单的勾子框架,这个框架将提供一个简易的途径去通过接口和能力去捕获API.文章试图揭示一些帮助你编程你自己的监控系统的方法。它提供了一种一对一的(译:和特定API一一对应的)的去构建方法,以勾挂在NT/2k和98/me Windows家族上的Win32 API函数的集合.为了简单起见,我不打算增加unicode支持。不过,通过对代码进行一些小改动你可以轻松完成这项任务。

监控应用程序提供了许多便利:

API函数控制
控制API函数是非常有用的,它允许开发者跟踪在API调用过程中特定的“隐藏”行为。它使得你可以对API的参数进行全面的检查,同时可以报告一些潜藏调用其后的问题。作为例子,有时对于控制一些内存相关的API以捕获内存泄漏,这会非常有用。

2 调试和逆向工程

    除了标准的调试API的方法外勾子技术理所当然的称得上是一种最受欢迎的调试机制之一。很多开发者利用API勾子技术去辨识不同的构件和他们之间的关系。API干预是一种捕获二进制可执行信息的功能强大的方法(译注:不同的工具开发的windows程序最终调用的可能是同样的API,所以这里说是捕获二进制信息,而不管其原来是用什么工具和什么环境开发出来的应用程序)

窥视操作系统内部
程序员常常非常喜欢去探窥操作系统的底层,并且乐于被祟拜为一个(底层)“调试员”的角色。勾子技术也是一种在解读帮助文档极少的API方面非常有用的技术。

3.向外部windows程序嵌入定制的模块可以扩展程序原始提供的功能。

注入勾子以调整原代码的执行路径可以提供一种简单方法去改变和扩展原有程序模块的功能。比如,有很多第三方的产品有时不能满足安全需求并且需要进行调整以满足你的特殊需要.监控应用程序允许开发者在API调用前后增加复杂的处理.这个能力对于更改已编译的代码非常有用。

勾子系统的功能要求

   在你开始实现任何类型的API侦控系统之前你必须作出一些不多但非常重要的决定。首先,你应该决定是否对一个单独的应用程序安装一个系统级监控引擎。比如,如果你仅仅想监控单独的一个应用程序,你没有必要去安装一个系统级别的勾子。但是如果你的工作是去跟踪所有TerminateProcess()或WriteProcessMemory()这样的函数,那么系统级的勾子是唯一的方法。影响你选择的因素在于特殊的情况和解决特定的问题。

一般API监控框架的设计

通常一个勾子系统至少由两个部分组成:一个勾子服务程序和一个驱动。勾子服务程序负责在合适的时刻注入驱动进入目标进程。它也管理驱动并且接收驱动在进行(API)干预时所返回的一些信息。这个设计很粗糙,不可置疑的它不能覆盖所有可能的执行情况。但不管怎么样,它概述了一个勾子框架的边界。

 

一旦你对监控框架的需求有详尽描述,有几点设计要点就应该考虑:

  你要对什么样的应用程序进行监控

  怎么样把DLL注入目标进程/使用哪种注入手段

注入技术

1.     注册表

为了把DLL注入使用了USER32.dll的进程,你只需简单把DLL名字加到下面注册表键值:

HKEY_LOCAL_MACHINE/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs

键值包括一个单独的dll名字或者一组用逗号或空格分开的dll名字。通过MSDN帮助文档可知,所有在这个键值上被windows应用程序加载的DLL都运行于当前登录会话中。有趣的时,这些DLL的加载是当作USER32初始化的一部分进行的.USER32读取这个提及的注册表键值,并调用LoadLibrary()启动dll和相应的DllMain()(译注:不了解DllMain和dll运行流程的请参见相关书籍).不过这个方法只用于用到User32.dll的应用程序。另一个限制是这个嵌入的方法也只适用于NT和2000操作系统.尽管这是一个无害的注入dll到一个windows进程的方法,仍然有几个缺点:

你必须要重启系统以激活/取消激活注入进程
你想注入的dll只能被映射进那些使用了user32.dll的进程,因此你不能期望你的dll被注入控制台应用程序,因为这些程序不能从user32.dll导入函数。
另外,你根本无法控制注入进程。这意味着,dll将被注入到每个GUI进程,不管你想不想。这通常有点冗余,特别是如果你只想监控少数几个应用程序。详细请参见[2]” Injecting a DLL Using the Registry”
   (译注:请查看注册表AppInit_DLLs的值,如果你的系统安装了杀毒软件,你发现了什么?^_^)

2        系统级Windows勾子

当然,一个非常受欢迎的注入DLL进目标进程的技术依赖于系统提供的windows勾子.正如MSDN提到的勾子是系统消息机制中的一个环节。一个应用程序可以安装一个定制的过虑函数去监视消息在系统的流通过程并且在消息到达目标窗口过程前处理某种类型的消息。

   勾子往往在dll中实现以满足系统勾子的基本要求。那种类类型的勾子有一个基本概念叫一个叫勾子回调过程的函数,这个函数在每一个在系统中的被“勾起”的进程的地址空间中执行。为了安装勾子,你必须用适当的参数调用SetWindowsHookEx()。一旦一个应用程序安装了一个系统级的勾子,操作系统就会把dll映射进勾子的客户进程的地址空间中。

图1

 

一个系统级的勾子仅仅通过执行SetWindowsHookEx()来注册一次。如果没有发错错误的话,函数返回一个勾子句柄。返回值还有用途,即在执行完你定制勾子回调用函数后,这个值要留给调用CallNextHookEx()时使用。在成功的调用SetWindowsHookEx()后,操作系统自动把dll(但不是必要是马上执行)注入到所有满足这个特定的勾子过滤要求的进程。让我们更进一步的看看接下来的这个“哑”WH_GETMESSAGE 过滤函数.

//------------------------------------------------------

// GetMsgProc

//

// Filter function for the WH_GETMESSAGE - it's just a

// dummy function

//-----------------------------------------------------------

LRESULT CALLBACK GetMsgProc(

  int code,       // hook code

  WPARAM wParam,  // removal option

  LPARAM lParam   // message

  )

{

  // We must pass the all messages on to CallNextHookEx.

  return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam);

}

一个系统级勾子被多个进程加载时不会共享相同的地址空间。比如勾子句柄sg_hGetMsgHook,它是虚拟在所有的地址空间中的:它作为SetWindowsHookEx()返回值被获取,并且作为参数传给CallNextHookEx().这意味着,和勾子服务程序程序一样,它的值必须是共享于被监控(译注:已经注入的)的进程中。为了使这个变量对所有进程可见,我们应该把它存在共享数据段中。下面举一个利用关键字#pragma data_seg()的例子。在此,我想指出的是,在共享数据段里的变量必须要被初始化,否则这个变量会被置于默认的数据段并且关键字#pragma data_seg()没有任何效果。

//-----------------------------------------------------

// Shared by all processes variables

//----------------------------------------------------------

#pragma data_seg(".HKT")

HHOOK sg_hGetMsgHook       = NULL;

BOOL  sg_bHookInstalled    = FALSE;

// We get this from the application who

// calls SetWindowsHookEx()'s wrapper

HWND  sg_hwndServer        = NULL;

#pragma data_seg()

你应该为dll的DEF文件加一个SECTIONS段

SECTIONS

  .HKT   Read Write Shared

或者用

#pragma comment(linker, "/section:.HKT, rws")

一旦勾子dll被加载进目标进程的地址空间就没有方法卸载它除非勾子服务程序调用UnhookWindowsHookEx()或被监控的应用程序自己关闭。当勾子服务程序调用UnhookWindowsHookEx()时,操作系统遍历一个内部列表---这个列表里包含所有被监控的所有加载了勾子dll的进程---同时dll的锁定值递减。当dll锁定值变为0,dll就自动的从被映射的进程的地址空间中释放出来。

 

这里列举了这种方法的一些优点:

这种机制被NT/2000和9xwindows家族支持,并也寄望会被未来的windows版本支持。
不像注入dll的注册表机制,这种方法允许当dll被认为不再需要时,勾子服务程序通过调用UnhookWindowsHookEx()卸载它。
       尽管我认为windows勾子技术是一种非常便利的注入技术,但是它仍然有它自己的缺点:

windows勾子会明显的降低整个系统的性能,因为他们增加了系统中必须处理的消息的处理工作量。
调试系统级勾子需要大量的工作。不管怎样,如果你使用多于一个的VC++实例同时运行时,it would simplify the debugging process for more complex scenario
最后但也是最重要的,这种类型的勾子影响整个系统的处理过程并且在某种不确定的情况下(应该说是一个bug)你必须重启机器来修复它。
3使用CreateRemoteThread()API函数注入DLL

这是我最喜欢的一种。不幸的是它只被NT和windows 2000操作系统支持。它看起来有点古怪,你也允许在Windows 9x调用这个API,但它任何事情都不做,仅仅返回一个NULL。

通过远程线程注入dll是Jeffrey Ritcher的主意并且在他的文章<< Load Your 32-bit DLL into Another Process's Address Space Using INJLIB>>陈述得很好。

  其中基本的概念很简单,但也很优雅.任何进程可以动态的用LoadLibrary装载Dll,问题是如果没有对外部进程的线程访问权,我们如何强制一个外部进程按照我们的意图调用LoadLibrary()?不错,有一个函数叫CreateRemoteThread(),可以解决创建远程线程的问题。你这就可以看到其中的窍门了,看一下那个线程函数了吧,就是以一个函数指针作为参数(LPTHREAD_START_ROUTINE)传给CreateRemoteThread的函数(译注:LoadLibary可以传给CreateRemoteThread,作为参数,而Dll名字可以作为另一个参数传给CreateRemoteThread)

DWORD WINAPI ThreadProc(LPVOID lpParameter);

这里就是LoadLibrary()API的函数原型

HMODULE LoadLibrary(LPCTSTR lpFileName);

看,他们(译注:LoadLibrary和ThreadProc)有”完全一样”的外表。他们使用同样的调用协定WINAPI,他们都接受一个参数,并且返回值的大小是一样的。这个匹配提示我们可以使用LoadLibrary()作为线程函数,也就是当远程线程被创建后被执行的函数。让我们看看下面的示例代码.

hThread = ::CreateRemoteThread(hProcessForHooking,

                                      NULL,

                                      0,

                                      pfnLoadLibrary,

                                      "C://HookTool.dll",

                                      0,

                                      NULL);

通过GetProcAddress()API,我们取得LoadLibrary()函数的的地址。巧妙的是,Kernel32.dll被映射到每个进程相同的地址空间,因此LoadLibrary()在每一个正在运行的进程地址空间里面有相同地址值。这就确保了传一个合法的指针(比如pfnLoadLibrary)作为参数传给CreateRemoteThread().

  因为要当作线程函数的参数使用,我们使用DLL的路径全名,并且转化为LPVOID类型。当远程线程被涣醒后,CreateRemoteThread()把DLL名字传给ThreadFunction(比如LoadLibrary).这就是关于用远程线程来达到进程注入目的的所有技巧。

用CreateRemoteThread()进入dll注入还有一个重要的事情需要考虑.每次在注入程序操作目标进程的虚拟内存并调用CreateRemoteThread()之前,注入程序首先打开进程,做法是调用OpenProcess()API并传输PROCESS_ALL_ACCESS标志作为它的参数。PROCESS_ALL_ACCESS标志是当我们想取得目标进程最大限度的权限才使用的。在这种情况下,如果是一些低值ID的进程,OpenProcess()将返回NULL。发生这种错误(尽管我们使用了合法的进程ID)是因为当前进程不是运行在有足够权限的安全上下文中。如果你对这种情况稍加思虑就会意识到这是说得通的。所有这些受到限制的进程都是操作系统的一部分,一个正常的应用程序不允许对它们进行操作。如果一个应用程序发生了BUG而意外中止了操作系统进程会发生什么情况呢?为了避免操作系统进程这种意外的瘫痪,一个应用程序必须要拥有足够的权限才可以执行会修改操作系统行为的API。为了通过OpenProcess()取得系统资源(比如smss.exe, winlogon.exe, services.exe等等),你必须授予进程调试权限(debug privilege).这个权限非常有用,它提供给进程访问系统资源的一种途径,而这种访问权在正常情况下是被禁止的。这种调整进程权限是很正常的工作,它可以用以下合理的操作实现:

a打开需要调整权限的进程的进程标志(process token),标志应该是允许的。

b给权限一个名字” SeDebugPrivilege”,我们应该找到它的本地LUID映射.这些权限可以通过名字标识,而且在Platform SDK文件winnt.h中能找到。

c为了允许” SeDebugPrivilege”权 限,调用AdjustTokenPrivileges() API调整进程标志。

d关闭通过OpenProcessToken()取得的进程标志句柄

想了解改变权限的细节请参见[10]”Using privilege”

4通过BHO插件注入

有时你只想往Inernet Explorer注入定制代码。幸运的是,微软提供了一个简易并且有完善文档陈述的途径达到这个目的,即BHO(Browser Helper Objects).BHO以COM dll组件的方式执行,并且一旦它被适当的注册时,每次打开IE时,这些实现了IObjectWithSite 接口的COM组件都会被加载

5通过MS Office插件注入和BHO相似,如果你需要向MS Office应用程序注入你自己你代码,你可以运用实现MS Office插件的标准机制。有很多的实例可以向你展示怎么样去实现这种插件。

干预机制

往一个外部进程地址空间注入dll是一个监视系统的关键部分,它提供了足够的机会去控制外部进程的线程活动。但是不管怎样,如果你想干预进程内部API函数的调用的话,光注入DLL还不够。文章的这部分将尝试去让你观望一下几个真实可用的勾子,并进行这方面的讨论。文章重点在于大略地对他们一个个的进行描述,展示他们的优点与不足。同时在勾子级别的实现上,分为两种机制监视API,一种是内核级的监视,另一种是用户级的监视。为了更好理解这两种级别的差别你必须要认识到win32子系统API和本地API的关系。下图描绘了这两个不同的勾子安装的位置并且以图示说明了模块间的关系以及他们对windows 2k的依赖性。

图2

  


在实现上他们的不同点是,内核(系统级)级的勾子伪装成了内核模式驱动,而用户级勾子通常使用的是用户模式DLL。

1.NT内核级勾子

  有几个方法可以实现针对内核态下的NT系统服务的勾子。最受欢迎的干预机制是最先被Mark Russinovich and Bryce Cogswell在他们的文章[3]<< Windows NT System-Call Hooking>>描述。他们的思想是仅仅在用户模式下对NT系统调用注入干预机制。这个技术功能强大,它提供了一种非常灵活的方法,在所有的用户态线程进入操作系统内核接受服务前这个位置上“勾挂”(译注:即勾子安装在这个点上处理系统调用)。

你也可以在文章<< Undocumented Windows 2000 Secrets>>找到更好的设计和实现 。在他的书<< Sven Schreiber>>中他解释了怎么样去构建一个勾子框架[5]。

另一个全面的分析和显赫的实现由Prasad Dabak在他的书《Undocumented Windows NT》提供[17]。不管怎么样,这些勾子的策略超出了这篇文章的范围。

 

2 win32用户级勾子

a. Windows subclassing.

   这个方法适用于应用程序的行为被新的窗口过程修改的情况。为了完成这个工作你需要调用SetWindowLongPtr() 用GWLP_WNDPROC作为参数并把打的窗口过程的指针传给它。一旦新的子窗口过程被安装了,每次windows分发消息到指定的窗口时,windows会去寻找和指定窗口相关联的窗口过程,而代替了原来的那个窗口回调函数。这种机制的缺点是这些子过程(subclassing)只适用于特定进程内。换句说话,一个应用程序不应该使用由另一个进程创建窗口子类。(In other words an application should not subclass a window class created by another process).

通常如果你使用插件技术(比如DLL / In-Proc COM 组件)去监控一个应用程序的话,这是可实现的,并且你能够取得你想替换窗口过程的那个窗口句柄。举个例子,我前段时间就写了一个简单的IE插件(BHO),替换了原来IE提供的弹出菜单,我使用的就是子过程(subclassing)

b.代理DLL(特洛伊DLL)

   一种简单的“黑掉”API的方法是仅仅用另一个DLL去替换掉原来的DLL,新的DLL和原来的有相同的名字并且可以导出和原来一样的(函数等)符号.这种偷换函数的技术花很少功夫就可以实现。偷换函数是基于DLL里的export段,这个段里的函数代理了对另一个DLL里的函数调用。

   你可以用这个指令完成这个任务:

 #pragma comment

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

可是,如果你采用这个方法的话,你应该负责提供对原来库中后来新版本的兼容支持。关于这点,详细参见[13a] section "Export forwarding" 和 [2] "Function Forwarders".

c.代码覆盖。

这里有几种关于代码覆盖的方法。其中一种使用CALL指令是改变函数地址。这种方法很困难,而且容易出错。在这个基本思想下,它是通过跟踪所有内存中的CALL指令,并以用户提供的地址替换原始的函数地址实现的。

另一种代码覆盖技术需要更加复杂的实现。概要的讲,这种方法的要领是找到原始API函数的地址,并且用JMP指令改变函数开始的若干字节使函数重定向到用户定制的API函数。这种方法非常巧妙,并且需要为私有的函数进行一系列的恢复和“勾挂”(hooking)操作。有一点很重要,必须要指出的是,如果函数不是在被监控(in unhooked)状态,同时有另一个对这个函数的调用,系统将不能捕获这第二次调用(译注:??).出现这个的主要问题在于它和多线程环境的规则有矛盾。但是,也有一种聪明的方法可以解决其中出现的问题,它提供

了一种复杂方法达到API干预的目的。感兴趣的话,翻阅[12]《Detours implementation》

D .通过调试器监视。

   一种替换API函数勾子技术的选择是往目标函数增加调试断点。然后这种方法有几个缺点。这种方法的主要问题是调试异常会挂起所有的程序线程,因此需要一个调试器处理这些异常。另一个问题是当调试器终止后,被调试的进程会自动被Windows中止。

e通过更改地址导入表进行监视

这个技术最先是由Matt Pietrek发表的,Jeffrey Ritcher([2] 《API Hooking by Manipulating a Module's Import Section》)和John Robbins([4] 《Hooking Imported Functions》)分别详细阐述。这种方法非常健壮,简单和易于实现。它也满足了勾子框架针对windows NT/2K,9x操作系统的要求。这种方法要领是依赖于windows优雅的PE文件格式的结构。为了读懂这种方法是如何进行的,你应该要熟悉PE文件格式,这是COFF(Common Object File Format)格式的扩展. Matt Pietrek在他著名的文章[4]《Peering Inside the PE》和[13]《An In-Depth Look into the Win32 PE file forma》中揭示了PE文件格式.关于PE文件格式,我将给出一个概观,只限于让你明白如何通过操纵地址导入表来进行监视.一般地,二进制PE文件代码段和所有数据段组织起来,使之符合虚拟内存中可执行代码的布局.PE文件格式由几个逻辑段组成,每个段维护OS加载器需要的特定类型的数据和地址

  .idata这个段需要你留心,它包含的就是地址导入表的内容.PE结构的这部分内容对构建基于修改IAT(地址导入表)的监视系统尤为关键.每一个遵循PE文件格式的可执行代码的在下面图中都有粗略的描述.

图3

 


程序加载器负责加载和程序相关的所有DLL到内存中去。因为DLL应该加载到的地址不为加载器提前所知的话,所以加载器就不知道每个需要加载的函数的真实地址。加载器必须要做一些额外的工作以确保每次能够成功的调用导入的函数。但是要在内存中遍历每个可执行映像并一个一个的寻找到导入函数的地址会花费不可接受处理时间还会使系统性能降低。那么,加载器是如何解决这个具有挑战性的问题的呢?要点是这样的,每一次调用导入函数时,调用会被指派到相同的地址,这个地址就是函数代码留驻内存的地址。每一次对导入函数的调用事实上是进行了重定向调用,即通过JMP指令经由IAT重定向。这样设计的好处在于加载器不需要搜索整个映像文件。这个解决方案看起来相当简单---- 它仅仅是从IAT里寻找所有的导入地址。这里有一个对简单的win32程序的PE文件格式的简单描述的例子,例子是从[8]《 PEView utility》得到帮助的。正如你所见,TestApp导入表包含两个来自GDI32.DLL的函数TextOutA()和GetStockObject().

图4

 

 

事实上,导入函数的勾子进程并不像乍一看那样复杂。简言之,一个使用修补过的IAT的干预系统(interception system)要找到导入函数的地址并用用户定制的函数重写以替换掉它。有一个重要的的要求,那就是新提供的函数调用方法和原来那个应该是一模一样的。在此列举一次替换过程的合理的几个步骤:

a找到每个要被进程导入的DLL模块和进程本身的地址导入段(import section)。

b找到描述DLL导入的IMAGE_IMPORT_DESCRIPTOR数据描述块。说实际点,我们需要用DLL名字搜索这个记录。

c 找到保存导入函数原始地址的位置IMAGE_THUNK_DATA

d用用户提供的函数地址替换掉找到的导入地址。通过更改在IAT里的导入函数的地址,我们确保了所有对调用被“勾挂”函数的调用重定向到预定的干预函数。

替换IAT里的指针有个问题,即.idata段没有必要一定是可写的段。这就要求我们必要确保.idata段是可更改的。这项任何可以使用API VirtualProtect()完成。

另一个值得注意的问题是关于API GetProcAddress()在windows 9x系统上的行为。当应用程序在调试器外调用这个API时,它返回(pointer to the function)某个函数指针。然而如果你在调试器内部调用它时,他实际上返回不同的地址,而不是像在调试器外部调用那样。引起这个不同是因为在调试器内部,每一个对GetProcAddress()的调用返回的是真实指针的转换块。GetProcAddress()返回的值指向一条PUSH指令,PUSH后面紧跟真实的地址(译注:??).这意味着,在Windows 9x上当我们循环转换块时时,我们必须检查得到的检查过的函数地址是不是一个PUSH指令(在x86平台上这指令是0x68)并相应地获取适当的函数地址值。
  Windows 9x不实现copy-on-write技术(译注:),因此操作系统试图不让调试进程进入地址高于2GB的函数。这就是GetProcAddress返回一个调试的转换块而不是真实地址的原因。

确定何时注入勾子DLL

当选择的注入机制不是操作系统功能的一部分时,开发者将面临一些挑战,这个章节便是要提示怎么样解决将会面临的这些问题。举个例子,当你用插件式的Windows勾子来注入一个dll时,怎么样注入本身并不是你所关心的问题。让每个满足勾子注入要求的将要运行的进程去加载DLL,这是操作系统的职责.[18]事实上,windows跟踪所有新启动的进程并且强制它们去加载勾子DLL.通过注册表管理DLL的注入(加载)与windows勾子函数是相似的(译注:??).使用这些插件方法的最大的优点是注入他们是操作系统职责的一部

和上面讨论的注入技术不一样,通过CreateRemoteThread注入需要对当前运行的进程进行维护.如果注入不够及时,会导致监视系统误漏一些已经被声明要干预的函数调用.有一个关键之处是,勾子服务程序实现了一个灵巧的机制,以接收每个新进程开始运行或中止时的消息通知.在这种情况下,一个被提出的方法是干预CreateProcess API函数家庭,并控制它们的每次执行.因此,当一个用户提供的函数被调用后,可以调用原始的CreateProcess,参数用dwCreationFlags和CREATE_SUSPENDED进行或运算的值,这样做意味着原始程序的目标线程将被置成挂起状态,这样勾子服务程序有机会注入自定制机器指令(by hand-coded machine instruction)的dll并接着用ResumeThread涣醒原程序.想看更加细节,[2]参考《Injecting Code with CreateProcess()》

  第二个监控进程执行的方法基于执行一个简单的设备驱动。这种方法提供了最大的灵活性,甚至更值得关注。Windows NT/2K提供了一个特别的API PsSetCreateProcessNotifyRoutine(),是在NTOSKRNL中导出的.这个函数允许增加一个回调函数,用于在任何时候一个进程被创建或删除时调用。想知道细节请参见参考章节的[11]和[15].

枚举进程和模块

有时我们愿意用CreateRemoteThread来进行DLL注入,特别是系统运行在NT/2K时.在这种情况下,当勾子服务程序开启时,它必须要枚举所有活动进程并把DLL注入到他们的地址空间。Windows 9x和windows 2000提供了Tool Help Library的插件式实现。另外Windows NT使用PSAPI实现相同的目的。我们需要一个方法允许勾子服务程序运行并且动态检测哪个进程的”helper”是有效的。这样系统能够决定哪个支持的是哪个库并调用相应适当的API。我将介绍一个面向对象的架构思想在NT/2K和9x[16]下实现一个简单的获取进程和模块的框架。我设计的类允许你根据特定的需求扩展这个框架。它的实现非常浅显易懂。

  CtaskManager实现系统处理者,负责创建特定库的实现的管理者(比如CpsapiHandler或CtoolhelpHandler),使其能够使用正确的进程信息提供库(分别是PSAPI和ToolHelp32). CtaskManager负责创建和填充一个容器对象,这个窗口维护有当前所有活动进程。当类CtaskManager实例化后,程序调用Populate()方法,它强制枚举所有的进程和DLL库并把它们保存到一个层次结构中,这个结构是由CtaskManager的成员变量m_pProcesses维护的。下面的UML图展示了这个子系统的类之间的关系。

图 5

 

 

必须要强调的重要一点是NT的Kernel32.dll没有实现ToolHelp32的任何函数。因此,我们需要使用运行时动态链接显式的链接他们。如果我们使用静态链接,不管应用程序是否已经尝试执行这些函数,在NT系统下,可执行代码将不能成功加载。欲知细节请参见我的文章《Single interface for enumerating processes and modules under NT and Win9x/2K.》

Requirements of the Hook Tool System

既然我已经对各种勾挂进程进行了一个概要的介绍,是时候讨论一下基本需要并探讨怎么样去设计一个特定的监视系统了。这里列出其中一些通过 Hook Tool System解决的问题:

提供一个用户级勾子系统监视任何通过名字导入的Win32 API函数
提供一种能力,可以向所有正在运行的进程通过windows勾子或CreateRemoteThread() API注入勾子驱动.这个框架应该提供通过INI文件启动的能力。
应用基于更改IAT(Import Address Table)的干预机制。
提出一个面向对象的可重用可扩展的层次结构。
提供一个高效的可调整(scalable)的监视API函数的机制.
满足性能要求.
提供一个可信的在勾子服务程序(server)和驱动(driver)之间传输数据的机制.
实现定制的TextOutA/W()和ExitProcess()API函数.
记录事件到日志文件.
设计和实现

文章的这部分讨论(监视)框架的关键组件和组件怎么样交互.这整套组件有能力捕获任何类型通过名字导入的WINAPI函数.

在我概述系统的设计之前,我想让你集中精力回想一下几个注入和”勾挂”的方法.

首先,有必须选择一种满足要求的注入方法,这种方法能够满足注入DLL驱动到所有的进程.所以,我设计了一种抽象的方法,这种方法采用两种注入技术,每种技术对应了相应的操作系统(比如 NT/2k或9x)和在INI文件中的相应设置.它们是系统级的勾子和CreateRemoteThread()方法.框架样本提供了通过Windows勾子和CreateRemoteThread把DLL注入到NT/2K系统的能力.如果注入,决定于INI初始化文件的选项,这个文件保存了所有系统设定选项.

另一个关键点是勾挂机制的选择.毫无意外的,我决定应用修改IAT作为监视win32 API的健壮方法.

为了实现这个梦寐以求的目标,我设计了一个由以下组件和文件组成的框架:

TestApp.exe 一个简明的win32测试程序,只是使用TesxtOut() API输出一段文本.这个程序的目的是为了展示它是如果被勾子”勾起”的.
HookSrv.exe 控制程序.
HookTool.DLL --- 实现为Win32 DLL的监视库文件.
HookTool.ini   一个配置文件.
NTProcDrv.sys  一个微型的用于监控进程创建和终止的Windows NT/2k内核模式驱动.这个组件是可选的并且,通过监控基于NT系统的进程执行来达到目的。

HookSrv是一个简单的控制程序,它的主要角色是加载HookTool.DLL并激活监视引擎(spying engine)。加载了DLL之后,勾子服务程序调用InstallHook()函数并传句柄给接受DLL所有消息的隐藏窗口。HookTool.DLL是勾子驱动并且是当前监视系统的核心,它执行真正的进程干预并提供三个用户定制的函数TextOutA/W()和ExitProcess()函数。

尽管文章强调的是在Windows内部的并且没有必要实现为面向对象,但我仍然决定把相关的操作封闭到可重用的c++类中。这个方案提供更高的灵活性,允许系统被进一步扩展。这也给开发者带来好处,使得他们可以在使用工程以为的私有类。

  下面给出UML类图,用于图解这些类之间的关系---这些类都是在HookTool.DLL中实现的。

图6

 

 

文章这个章节我将集中精力阐述HookTool.DLL的类设计。在开过程中,给类分派职责是一个重要部分,每个给出的类要封装特定的功能并代表一个特定的逻辑实体。CmoduleScope是系统的主要入口,它执行时只有一个实例,并且是线程安全的模式。执行时,它的构造函数接受三个在DLL共享数据段内的指针,这些指针将在所有的进程中使用。这样,由于遵循封装的规则,这些系统级变量的值可以轻松的在类的内部进行维护。当应用程序加载HookTool library时,DLL创建CmoduleScope的一个实例,用于接受DLL_PROCESS_ATTACH消息通知。这一步仅仅完成CmoduleScope实现的初始化工作。在CmoduleScope对象的构造中,一个重要的片段是创建适当的注入对象。注入对象如何创建是在分析完HookTool.ini配置文件后才决定的,而之后也通过Scope段的值决定了UseWindowsHook参数的值。万一系统是运行在Windows 9x下的,这个参数的值就不会被系统检测到,因为Windows 9x不支持通过远程线程注入。

在主处理对象实例化之后,程序会调用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_pbHookInstalledr的值检查这个方法是否已经被勾子服务程序调用过。如果已经事先被勾子服务程序调用过了,它只简单的间接的把sg_bHookInstalled标记置为真—-真值是提示勾子服务程序已经安装好。

   下一步操作是,勾子服务程序通过单独调用DLL的导出函数InstallHook()激活引擎。实际上,这个调用是一个“委托”调用,它执行的是CmoduleScope的方法InstallHookMethod().这个方法的主要目的是强制目的进程加载或卸载HookTool.DLL.

// 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;

}

HookTool.DLL提供了两种把自己注入到外部进程地址空间的机制。一种是使用Windows勾子,另一种是通过CreateRemoteThread API注入DLL。在这个监视系统的构架中,它定义了一个抽象类Cinjector,这个抽象类暴露一个用于注入和卸出DLL的纯虚函数.类CwinHookInjector和CremThreadInjector继承于相同的基类Cinjector.可就这样,他们提供纯虚函数InjectModuleIntoAllProcesses()和EjectModuleFromAllProcesses()两个不同的实现,这两个函数都定义为Cinjector的接口函数。

  CwinHookInjector类实现了Windows勾子的注入机制,它通过下面调用安装过滤函数(即安装勾子)

// Inject the DLL into all running processes

BOOL CWinHookInjector::InjectModuleIntoAllProcesses()

{

  *sm_pHook = ::SetWindowsHookEx(WH_GETMESSAGE,

                                 (HOOKPROC)(GetMsgProc),

                                 ModuleFromAddress(GetMsgProc),

                                 0 );

  return (NULL != *sm_pHook);

}

正如你所见,CwinHookInjector要求向操作系统安装WH_GETMESSAGE勾子。勾子服务程序仅仅会执行这个方法一次。SetWindowsHookEx()的最后一个参数是0,因为GetMsgProc()是设计为一个系统级的回调勾子的。GetMsgProc()这个回调函数将被系统在每次要处理一个特定消息时所调用。有意思的是,如果我们不想监视消息处理,我们需要提供一个回调函数GetMsgProce()的“哑”(dummy)实现。我们这样实现只是为了能够从操作系统那获取一个自由的注入机制.

   当调用SetWindowsHookEx()时,操作系统会先检查导出GetMsgProc()函数的DLL(HookToll.DLL)是否已经映射到了所有的GUI进程中。如果指定的DLL还没有加载,Windows会强制这些GUI进程映射它。有意思的是,系统级的勾子DLL不应该在对应的DllMain()里立即返回FALSE.那是因为操作系统检测DllMain()的返回值并一直尝试加载DLL直到相应的DllMain()最终返回TRUE.

   一个差别很大的方法在CremThreadInjector类中阐述。这个实现是基于使用远程线程注入的。CremThreadInjector类设计一个接收进程创建和中止消息的方法,以此来扩展对windows进程的维护。它还内聚一个CntInjectorThread对象,用于捕获内核模式的驱动发送过来消息通知。因此,每次当一个进程被创建时,CNtInjectorThread ::OnCreateProcess()就会被处理一次,相应的当进程退出运行时,CNtInjectorThread ::OnTerminateProcess()也会被自动调用.不像windows勾子,这种方法依赖于远程线程,当一个新进程被创建时,还需要(注入程序)手动注入DLL。当新进程开启时,这样监控进程活动提供给我们一个简单的修改(进程行为)的途径。

CntDriverController类实现了对管理系统服务和驱动的API函数的封装功能,设计它是为了掌控内核模式驱动NTProcDrv.sys的加载和卸载。具体实现将在稍后章节中讨论。

  当HookTool.DLL被成功都注入到一个特定的进程之后,在DllMain()中会调用ManageModuleEnlistment()方法。回忆一下我前面介绍的这个方法的实现,它通过CmoduleScope的成员m_pbHookInstalled检测了共享变量sg_bHookInstalled的值。

    因为勾子服务程序在初始化时已经把sg_bHookInstalled的值设为TRUE,监视系统检查当前的那个应用程序是不是必须要被“勾挂”(监视),如果是,则实际上是对激活针对这个进程的监视引擎.

  开启了监视引擎是在CModuleScope::InitializeHookManagement()中实现的。这个方法的目的是为一些重要的函数比如LoadLibrary()和GetProcAddress API家庭安装勾子。通过这种途径,可以监视进程初始化后对DLLS的加载。每次当一个新的DLL准备要映射时,有必要先查找它的“导入表”,以确保监视系统不会错过对任何想捕获的函数的调用。

  在InitializeHookManagement()方法结束时,我们对真正想欲监控的函数进行一些初始化工作。

   因为示例代码中阐述的怎么捕获多个用户提供的函数(译注:欲监控的API),我们必须对每个函数提供单独的监控实现。这意味着,用这种方法时,不能只是改变IAT中不同导入函数的地址以指向唯一的通用的干预函数.监控函数需要知道调用指向的是哪个函数。有一点也非常关键的是,指向的函数调用方式必须和原来WINAPI原型精确匹配,否则堆栈就会被破坏。举个例子,CmoduleScope实现三个静态方法MyTextOutA(),MyTextOutW() 和 MyExitProcess().一旦HookTool.DLL被注入到进程的地址空间并且监控引擎就被激活,每次当有对原始API TextOutA()的调用时,CModuleScope:: MyTextOutA()会替换原来的调用。

   已经提出的这个监控引擎的设计本身非常有效并且提供了很大的灵活度。不管怎样,它适合于大多数情况,在这些情况中,欲干预的函数集是提前知道的并且他们数量是有限的。

  如果你想对系统增加新的勾子,你只需要简单的声明并实现干预函数,就像我已经做好的MyTextOutA()和MyExitProcess那样。然后你需要在InitializeHookManagement()的实现加注册他们。

    对于要实现有控制外部进程需求的系统来说,干预和跟踪进程执行是非常有用的机制。当一个进程开启时,把信息通知到感兴趣的第三方是关于开发监控系统和系统级勾子的一相经典问题。Win32 API提供了有用的库集(PSAPI 和ToolHelp [16]),允许你枚举系统当前运行的进程。尽管这些API功能相当强大,但是当一个进程开始或终止时,你并不能通过他们获得消息。幸运的是, NT/2K提供了一组由NTOSKRNL导出API,在Windows DDK文档《Process Structure Routines》有详细描述.这些API其中有一个是PsSetCreateProcessNotifyRoutine(),能够让你注册一个系统级的回调函数,在每次有新的进程创建,停止或被强行终止时,操作系统都会调用这个函数。这个被提及的函数可以简单的通过实现一个内核模式的驱动和用户级的控制程序来监控所有进程。这个“Windows进程的守护者” NTProcDrv提供了最小化的功能要求,可用基于NT系统的进程监控。欲知更详细的细节,请参见文章[11]和[15].这个驱动(NTProcDrv)的代码可以在NTProcDrv.c中找到。因为是在用户模式实现驱动的加载和卸载,当前登录的用户必须要用系统管理员的权限,否则你将无法安装驱动,进程的监控也就被打断。一个退一步的方法是,你可以手动以系统管理员的身份手动安装它或执行windows 2k提供的HookSrv.exe.以一个不同的用户身份运行。

最后的也是最重要的,这里提供的工具(即编译出来的可执行文件)可以简单的通过改变INI文件(HookTool.ini)的配置进行管理。这个文件决定是使用Windows勾子(9x和NT/2k有效)还是使用CreateRemoteThread()(只在NT/2k下有效)进行注入.文件也可以提供一个方法去指定哪个进程应该“勾挂”和哪个不要。如果你想监控某个进程,则在[Trace]有一个选项Enabled,允许你记录系统活动。这个选项允许你使用ClogFile类暴露的方法来报告丰富的错误信息。实际上,ClogFile是线程安全的实现并且你不用去关心和访问系统资源(比如log file)相关的同步问题。欲知细节,请参见ClogFile类和HookTool.ini文件的内容。

示例代码

   这个工程用VC6++SP4编译通过,并要求有SDK平台。在Windows NT产品的环境下,你需要提供PSAPI.DLL,以实现CtaskManager类。在你运行示例代码之前,确保HookTool.ini文件已经根据你的特定需要进行配置。

  如果喜欢更加底层的东西,并且对未来内核模式驱动NTProcDrv代码的开发感兴趣的话,需要安装WINDOWS DDK.

超出本文范围的讨论

为了简单起见,一些我在此文中有意不提的主题如下:

a.      监控本地API调用

b.     监控Windows 9x系统进程执行的驱动

UNICODE支持也没有提,尽管你也能监控导入的UNICODE API.

总结

  这篇文章并不提供针对所有API监控主题的完整的指南,显然是因为还缺少很多细节的东西。不管怎么样,我已经尝试了在这个篇幅的文章里用足够重要的信息来帮助那些对用户模式Win32 API监控感兴趣的读者。

参考

[1] "Windows 95 System Programming Secrets", Matt Pietrek
[2] "Programming Application for MS Windows" , Jeffrey Richter
[3] "Windows NT System-Call Hooking", Mark Russinovich and Bryce Cogswell, Dr.Dobb's Journal January 1997
[4] "Debugging applications , John Robbins
[5] "Undocumented Windows 2000 Secrets" , Sven Schreiber
[6] "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format" by Matt Pietrek, March 1994
[7] MSDN Knowledge base Q197571
[8] PEview Version 0.67 , Wayne J. Radburn
[9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB" MSJ May 1994
[10] "Programming Windows Security", Keith Brown
[11] Detecting Windows NT/2K process execution Ivo Ivanov, 2002
[12] "Detours" Galen Hunt and Doug Brubacher
[13a] "An In-Depth Look into the Win32 PE file format", part 1, Matt Pietrek, MSJ February 2002
[13b] "An In-Depth Look into the Win32 PE file format", part 2, Matt Pietrek, MSJ March 2002
[14] "Inside MS Windows 2000 Third Edition" , David Solomon and Mark Russinovich
[15] "Nerditorium", James Finnegan, MSJ January 1999
[16] Single interface for enumerating processes and modules under NT and Win9x/2K. , Ivo Ivanov, 2001
[17] "Undocumented Windows NT" , Prasad Dabak, Sandeep Phadke and Milind Borate
[18] Platform SDK: Windows User Interface, Hooks

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/liuzongqiang/archive/2008/03/19/2195931.aspx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值