天天看点

HOOK启思录

 http://blog.csdn.net/xiammy/archive/2006/11/19/1396897.aspx

HOOK启思录---前言:HOOK是一种思想

在很多人眼里,Hook都是高级程序员才会使用的技术。一年前,我也是。每每看到别人使用了Hook技术就很嫉妒。是的,说嫉妒一点都不夸张。

刚开始,不知道Hook到底是什么技术。或者说,Hook到底是做什么的。Hook的英文是钩子,但是,说实话,我的中文意识中,怎么也不能将钩子和Hook划等同符号。 一个钩子,它在等待我们去往上面挂上我们想要的东西。这点,从最终结果看。确实和这个技术很符合。可问题是,那些钩子在什么地方?我们家门口倒是有几个挂衣服的钩子,可是我要挂其他物件的钩子在哪里呢? 最常听说的就是API HOOK了。有句话说得很有道理,HOOK API是一个永恒的话题。网络上,不知有多少人在追捧这个技术。毕竟,喜欢探险是我们的本能。深入别人的系统,获取别人的信息,修改别人的行为,一直都是我们梦寐以求的。 API HOOK又分为13种,具体参考MSDN。这些当中很多在你对系统内核很感兴趣的时候,非常有用。不过我们经常说的钩子,都是鼠标钩子、键盘钩子、消息钩子。 如果我们只满足于这些钩子,那么钩子也不会发展得这么兴旺了。事实上,除了微软提供的钩子函数外,通过钩子的实现原理,我们还可以实现其他类似的钩子。 我对钩子的深入了解和大多数人可能相反,这得感谢我的超同事。我深入了解钩子的时候,是从Delphi的代码钩子开始的。 Delphi的VCL中有些函数实现可能比较低效,但是又没有提供修改的机会。怎么办呢?大家知道,函数在内存中的表现,就是一段代码,其中不连续部分,使用跳转指令(这点很重要)完成。就是说,我们的代码有这个特性: 代码片段(A+B) = 代码片段A + jump + 代码片段B 正是因为这个特性,我们可以通过一些添加一个跳转,让原想调用“代码片段(A+B)”的代码,实际调用“代码片段A + jump + 代码片段C”。这个特性先简单的说明,以后会详细描述。 话说回来,我们解决低效的方法,就是找一个高效的代码,HOOK原有的低效代码。【我这里希望大家接受我的一个建议,不要关注HOOK细节,如果技术可行,我们就简单说明为HOOK。就像这里我这么描述一样】 慢慢的,发现有时候VCL的类库有一些BUG。发现也可以使用类似方法实现。当然了,这里需要注意类方法和普通方法的参数差异(多一个Sender参数)。 有一个典型的例子,就是在Delphi中,TObject提供了从对象到接口的转换服务(as操作符),但是并没有提供接口到实现对象的转换服务,即由一个接口指针,得到实现此接口的对象的服务。 如果我们知道实现此接口的类,我们可以通过获取对象指针和接口指针的偏移,来获得类实例指针。(偏移请参考TObject的InitInstance函数) 但如果我们从高一点的角度来看,假设我们认为TObject必须有这个服务,那么TObject的设计就是有缺陷的。如果我们修复这个错误呢?那么,这个设计不就符合我们的要求了嘛? 下面是我的超同志曾经提出的一种改写方式: 请注意和原有代码差异: type  TGetInterfaceFunc = function(Self: TObject; const IID: TGUID; out Obj): Boolean; const  IID_QueryObject: TGUID = '{AD28C649-509D-40F9-B02B-70CF197ECB3C}'; var  OldGetInterface: TGetInterfaceFunc; function GetInterface(Self: TObject; const IID: TGUID; out Obj): Boolean; var  InterfaceEntry: PInterfaceEntry; begin  if IsEqualGUID(IID_QueryObject, IID) then  begin     Pointer(Obj) := Self;     Result := True;  end  else begin     Result := OldGetInterface(Self, IID, Obj); // OldGetInterface是在HOOK的时候,记录 // 的原有函数的地址。  end; end; 方法是通过对接口中QueryInterface的基础方法GetInterface进行改写,如果传入的IID等于IID_QueryObject的话,就返回对象指针。最后就是针对VCL,HOOK上面这个方法。 细心的你可能发现,上面列举的例子,我刻意地没有去描述HOOK的原理,而重点在说HOOK的应用,或者说,是在使用HOOK技术来解决问题。 事实上,我更想说的是,我们使用HOOK这种思想来解决问题。s每每想起HOOK的巧妙解决方式,我都非常振奋。也这是因为这些振奋,才使我想写一系列关于HOOK思想的文章。希望大家都能使用HOOK思想去思考问题,而不是只是关注于HOOK技术本身。 现在正在流行的AOP(面向方面编程)思想,也是基于HOOK技术发展起来的。但是,AOP一直强调AOP不只是HOOK!实际上,AOP更注重基于HOOK的切片思想。这正和我的想法一致。 我的最终目标不是把大家引向AOP,而是让大家逐步了解到基于HOOK的一些原则和实践。中间有些章节载自别人的文章,只是希望能够比较详细的描述HOOK思想。 希望读完这系列文章,大家能够理解一句话:HOOK是技术,HOOK更是思想。 HOOK启思录---第一章 HOOK的发展 非常遗憾,HOOK的发展史不是那么清晰可见。事实上,HOOK到底是什么,很多人的说法都不一样。 最早是在操作系统中出现的HOOK概念。在Unix/Linux/Windows中都有类似概念。当时提出的目的在于,允许用户在系统调用过程中,插入自己的代码处理特殊事情。典型的HOOK就是用自己的功能替换原有的函数点,在处理完成之后,又恢复原有的函数点。(这里“点”就是表示一个可以使用HOOK勾住的位置)。 下面是《关于钩子》中,描述的Windows是中的钩子: 在 Windows中,钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。 钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。 其实从中可以看出,这些钩子应该和回调函数(CALLBACK FUNCTION)是一种技术。只不过,Windows专门将此类系统消息处理机制叫HOOK。 Falls先生曾经提醒我,在UNIX中的HOOK更纯粹。是的,UNIX中LD_PRELOAD环境变量(Soloris系统为例)针对所有系统调用做了一个类似代理功能,如果你想改变某个系统调用,就可以在这个代理中注册,动态调用的时候,会先从代理里寻找自定义代码,然后再找系统预定义代码。 我愿意使用图来表示这里面的差别。第一种Windows中的HOOK概念,其实是说系统已经在可以扩展的地方,预先放好了一些钩子在那里,你可以在需要的时候,挂上自己想要的东西。

HOOK启思录

相对于Windows来说,UNIX中,基本思想是一致的,细微的差距在于,扩展点的提供上。UNIX几乎提供了所有可能的扩展点。由于太多了,所以换句话说,也可以说成UNIX没有提供扩展点。这些扩展点,是用户在使用的过程中自己去发现,自己去勾住的。

HOOK启思录

这些就是其中的差距。而HOOK思想的发展也正是依照UNIX这类路线走下去的。在Windows中,大家还在使用的HOOK,除了定义好消息钩子,最典型的就是dll的导出表钩子。可以改变系统dll的函数的导出函数的地址,来勾住某些特定操作。必然监控文件创建等等操作,就可以勾住CreateFile这个API函数。 事实上,在初期,很多钩子都是发展用来修改别人的系统的行为的。后来钩子也用来修改本系统的行为。这主要源于软件系统的长足发展及框架系统的复杂度不断提升。现在的软件系统对我们来说,已经不大可能了解所有的部分,而且基类框架的稳定性不允许我们去修改它们。在这种情况下,HOOK思想带入到解题思路中。可以使用钩子来解决一些扩展或修复一些系统设计缺陷。AOP的思想和这类应用很相似。 整个HOOK的发展,从开始的回调函数模式,到扩展系统行为,再到扩展本系统行为。这个过程,有一点大家认识越来越清晰。HOOK是在合适的位置插入自己的代码而扩展或改变原有系统的(外系统或本系统)的行为的。HOOK技术也越来越主动的寻找扩展点,而不仅仅是使用原有系统提供的扩展点。甚至已经派生中一种系统开发方式AOSD。 HOOK的未来在哪里?随着.NET和JAVA对AOP的支持,HOOK技术越来越被框架直接支持。相信不远的未来,HOOK会直接作为一种解题思想而推广,并且框架会提供一系列HOOK模式来解决类似问题。 HOOK启思录---第二章 HOOK的根源 我一直在避免写HOOK的技术实现,想尽可能地将一些看不到的东西收集起来。相对于了解一个成熟的技术,还不如去了解其中的思想吧。我是这样认为的。 这一章会暂时远离HOOK本身,我们会尝试着去思考这些技术出现的根源。 很多人象我一样,从OP的衰败中走向OO的繁荣。软件的复杂度也是突飞猛进。当年“没有银弹”的断言,非常准确地说明了软件的发展趋势,当新技术发展的时候,软件的复杂度也随之发展。我们现在的软件,已经不再是OP时代的软件了。 大家需要的不再是一段会计算的代码。大家需要图形界面,大家需要管理流程,大家需要信息共享! 假设我们以前那些单一功能的软件都可以比喻成一条路的话。它非常简单,你只要考虑从A如何走到B就可以了。

HOOK启思录

后来,我们的软件慢慢复杂起来,他们已经不再满足于一个功能,进行多功能整合的软件比比皆是。事实上,那个时候,好多软件都是一个功能的集合。

HOOK启思录

随着技术的发展,我们不再满足于这些简单功能的叠加。事实上,正是这个时候,我们的软件产业才真正发展起来。在中国,2000年以来,软件企业开始尝试工业化生产技术,发展速度明显加快,与国际差距明显缩小。 随着工业化发展,我们的软件复杂度是急剧的增加。它的功能需求也非常多。也许正如下图说画的那样,各种功能交错。

HOOK启思录

遗憾的是,如果软件只是需要这种层次的复杂度,我们就会很开心了!你时常会发现,在某些时候,你从A走到B的过程中,需要走到M到N上。然后在拐道走去P到O,然后再到B。这就是我们业务的交错性。 比如,我们现在有两个功能,一个是拷贝文件,一个是日志。从面向对象的角度来看,文件的维护工作和日志的维护工作都可以使用独立的类来完成:

HOOK启思录

上面两个类都能完成独自的相关业务,就正如我们上面描述的一段独立功能类似。可以假想,我们每个类,都是封装的相关业务功能。 那么剩下来我们就会发现一个非常严重,但是又很少被人提起的事:并不是写完两个类,我们的工作就完成了! 我们虽然是面向对象编程,但是最终总会发现,对象写完的时候,并不是功能完成的时候!为什么?因为我们的功能往往是交错的,就拿这个例子,写文件的时候也可能需要写日志!这些都是面向对象无法使用对象的概念来解决的。 于是从一开始,面向对象就有事件的概念提出来(Event)。这个概念的提出,并不仅仅是炒作一个概念,它的实现机制虽然和回调函数一样,但是思想却又是那么无可奈何! 如果没有事件,那么我们会如何编程呢?我想下面的这种道路图可能大家能从中看到什么。

HOOK启思录

多说几句,这其实在我们在编程中经常犯的错误,如果AB和CD都是我们自己编写的(如果AB是类库提供的另当别论),我们往往将AB和CD的交叉业务部分直接写成关联代码。即AB和CD会变得互相依赖! 从社会实践,我们会知道,要提高AB和CD的效率,他们的道路是不能重叠的。可以采取高架桥、隧道、地下通道等等措施。高速公路永远不可能和别人公用交叉路口。

HOOK启思录
HOOK启思录

这些交错的方式,其实就是我们软件中可以借鉴的方式啊!以前提供的回调函数,现在提供的事件,以及AOP中提出的HOOK方式都是!这类方法其实就是我们开始统一说的HOOK。 因此采用这些方式的本质源由是软件的复杂度,导致我们在编写独立功能的时候,不能完全预测或根本不能,预测到其他功能是如何扩展的。因此预先留一些可以扩展点,或者某些可以扩展的方式,来完成交错功能的实现。 上面这些的讲述,好像和HOOK没关系,其实可以更深入的帮助我们理解使用HOOK的意义所在。  

HOOK启思录---第三章 HOOK的应用模式 观察模式 在HOOK很多应用中,有一类应用最为常用,那就是观察模式。对于信息获取的要求,在很多时候都是非常的重要的功能。在很多时候,我们都需要根据一定量的信息去判断如何决策。正如打仗一样,没有一定量的有意义的数据,就是摸黑,只能死路一条。 像Windows提供的SetWindowHook就是典型地为这类应用准备的。而且这也是最普遍的用法。 这个模式的特点是,在事情发生的时候,发出一个通知信息。观察者只可以查看过程中的信息,根据自己关心的内容处理自己的业务,但是不可以更改原来的流程。 如全局钩子中,经常使用的鼠标消息、键盘消息的监视等应用。金山词霸屏幕取词的功能是一个典型的应用(具体技术可以参考此类文章)。 注入模式 这个模式和观察模式最大的不一样的地方在于,注入的代码是为了扩展原始代码的功能业务。插件模式是此类模式的典型案例。 不管瘦核心的插件系统(如Eclipse)还是胖核心的插件系统(如Delphi、Visual Studio等IDE环境),其对外提供的插件接口都是为了扩展本身系统的功能的。 这种扩展的应用方式的典型特点,就是新的扩展代码和原来的代码会协调处理同类业务。 还有一些好的例子。我们曾经有一个应用,就是替换ReadFile和WriteFile,用以做到对所有文件操作的加密工作。这种应用和插件的差异在于被注入方是主动还是被动的。但是都是在完成同一类业务。 替换模式 如果针对应用目的不同,可以叫修复模式或破解模式。前者是为了修改系统中的BUG,后者是为了破解原有系统的限制。 比如我在前言中提到的,Delphi的VCL中,TObject并没有提供从接口到对象的转换服务,如果我们使用了此类模式,就可以让我们获得这种非常好的服务。对于一个设计或实现上有缺陷的原有系统(一般不能直接修改代码)来说,HOOK机制往往是很好的选择。 这类称之为修复模式非常好。 很多黑客也使用此种模式,必然替换访问加密锁的DLL中的导出表,替换成自己的函数,这样跳过对软件的控制代码。我们这里不是重点讲述软件破解,所以不继续深究其破解原理。但是,可以强调的是,这类应用的难点是,找出函数的参数。 这类模式的特点是,原有的代码会被新的代码所替换。   前面三个是基本模式,还有很多和实际应用相关的模式。 集权模式 此类模式的出现,大都是为了在全部系统中,统一处理某类事情。它的特点不在于注入的方式,而在于处理的模式。 这个模式,大都应用到某类服务上,比如键盘服务,鼠标服务,打印机服务等等特定服务上。通过统一接管此类服务的访问,限制或者协调对服务的访问。 比如键盘锁功能的实现,就是暂时关闭键盘的所有应用。 这类模式的特点主要会和特点服务有关联。 修复模式 替换模式的一种,这里强调的是其应用的目的是为了修复或扩展原有系统的功能。 破解模式 替换模式的一种,这里强调的是其应用的目的是为了跳过原有系统的一部分代码。如加密检测代码,网络检测代码等等。 插件模式 注入模式的一种,在系统的内部直接依靠HOOK机制进行扩展业务功能。 共享模式 这类应用中,经常是为了获取对方的数据。必然我希望获取对方系统中,所有字符串的值。可以通过替换对方的内存管理器,导出所有字符串。 这个应用比较特殊。不过其特点在于,目的是达到系统之间的数据共享。 其实现,可能是观察模式,也可能是替换模式。   其他不能一一详述,欢迎大家一起补充

继续阅读