Chinaunix首页 | 论坛 | 博客
  • 博客访问: 44119
  • 博文数量: 5
  • 博客积分: 840
  • 博客等级: 准尉
  • 技术积分: 230
  • 用 户 组: 普通用户
  • 注册时间: 2008-01-09 18:33
文章分类
文章存档

2011年(1)

2008年(4)

我的朋友
最近访客

分类: WINDOWS

2008-03-20 12:17:32

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,参数用dwCreationFlagsCREATE_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 9xwindows 2000提供了Tool Help Library的插件式实现。另外Windows NT使用PSAPI实现相同的目的。我们需要一个方法允许勾子服务程序运行并且动态检测哪个进程的”helper”是有效的。这样系统能够决定哪个支持的是哪个库并调用相应适当的API。我将介绍一个面向对象的架构思想在NT/2K9x[16]下实现一个简单的获取进程和模块的框架。我设计的类允许你根据特定的需求扩展这个框架。它的实现非常浅显易懂。

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

5



 

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

Requirements of the Hook Tool System

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

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

设计和实现

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

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

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

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

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

  1. TestApp.exe 一个简明的win32测试程序,只是使用TesxtOut() API输出一段文本.这个程序的目的是为了展示它是如果被勾子勾起.
  2. HookSrv.exe 控制程序.
  3. HookTool.DLL --- 实现为Win32 DLL的监视库文件.
  4. 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



 

 

阅读(1044) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:windows API 勾子内幕初探(下)

给主人留下些什么吧!~~