HOOK

摘要: 本文针对HOOK技术在VC编程中的应用进行讨论,并着重对应用比较广泛的全局HOOK做了阐述。

  引言

  Windows操作系统是建立在事件驱动机制之上的,系统各部分之间的沟通也都是通过消息的相互传递而实现的。但在通常情况下,应用程序只能处理来自进程内部的消息或是从其他进程发过来的消息,如果需要对在进程外传递的消息进行拦截处理就必须采取一种被称为HOOK(钩子)的技术。钩子是Windows操作系统中非常重要的一种系统接口,用它可以轻松截获并处理在其他应用程序之间传递的消息,并由此可以完成一些普通应用程序难以实现的特殊功能。基于钩子在消息拦截处理中的强大功能,本文即以VC++ 6.0为编程背景对钩子的基本概念及其实现过程展开讨论。为方便理解,在文章最后还给出了一个简单的有关鼠标钩子的应用示例。

  钩子的基本原理

  钩子的本质是一段用以处理系统消息的程序,通过系统调用,将其挂入到系统。钩子的种类有很多,每一种钩子负责截获并处理相应的消息。钩子机制允许应用程序截获并处理发往指定窗口的消息或特定事件,其监视的窗口即可以是本进程内的也可以是由其他进程所创建的。在特定的消息发出,并在到达目的窗口之前,钩子程序先行截获此消息并得到对其的控制权。此时在钩子函数中就可以对截获的消息进行各种修改处理,甚至强行终止该消息的继续传递。

  任何一个钩子都由系统来维护一个指针列表(钩子链表),其指针指向钩子的各个处理函数。最近安装的钩子放在链的开始,最早安装的钩子则放在最后,当钩子监视的消息出现时,操作系统调用链表开始处的第一个钩子处理函数进行处理,也就是说最后加入的钩子优先获得控制权。在这里提到的钩子处理函数必须是一个回调函数(callback function),而且不能定义为类成员函数,必须定义为普通的C函数。在使用钩子时可以根据其监视范围的不同将其分为全局钩子和线程钩子两大类,其中线程钩子只能监视某个线程,而全局钩子则可对在当前系统下运行的所有线程进行监视。显然,线程钩子可以看作是全局钩子的一个子集,全局钩子虽然功能强大但同时实现起来也比较烦琐:其钩子函数的实现必须封装在动态链接库中才可以使用。

  钩子的安装与卸载

  由于全局钩子具有相当的广泛性而且在功能上完全覆盖了线程钩子,因此下面就主要对应用较多的全局钩子的安装与使用进行讨论。前面已经提过,操作系统是通过调用钩子链表开始处的第一个钩子处理函数而进行消息拦截处理的。因此,为了设置钩子,只需将回调函数放置于链首即可,操作系统会使其首先被调用。在具体实现时由函数SetWindowsHookEx()负责将回调函数放置于钩子链表的开始位置。SetWindowsHookEx()函数原型声明如下:

HHOOK SetWindowsHookEx(int idHook;
HOOKPROC lpfn;
HINSTANCE hMod;
DWORD dwThreadId);

  其中:参数idHook 指定了钩子的类型,总共有如下13种:

   WH_CALLWNDPROC 系统将消息发送到指定窗口之前的"钩子"
   WH_CALLWNDPROCRET 消息已经在窗口中处理的"钩子"
   WH_CBT 基于计算机培训的"钩子"
   WH_DEBUG 差错"钩子"
   WH_FOREGROUNDIDLE 前台空闲窗口"钩子"
   WH_GETMESSAGE 接收消息投递的"钩子"
   WH_JOURNALPLAYBACK 回放以前通过WH_JOURNALRECORD"钩子"记录的输入消息
   WH_JOURNALRECORD 输入消息记录"钩子"
   WH_KEYBOARD 键盘消息"钩子"
   WH_MOUSE 鼠标消息"钩子"
   WH_MSGFILTER 对话框、消息框、菜单或滚动条输入消息"钩子"
   WH_SHELL 外壳"钩子"
   WH_SYSMSGFILTER 系统消息"钩子"

  参数lpfn为指向钩子处理函数的指针,即回调函数的首地址;参数hMod则标识了钩子处理函数所处模块的句柄;第四个参数dwThreadId 指定被监视的线程,如果明确指定了某个线程的ID就只监视该线程,此时的钩子即为线程钩子;如果该参数被设置为0,则表示此钩子为监视系统所有线程的全局钩子。此函数在执行完后将返回一个钩子句柄。

  虽然对于线程钩子并不要求其象全局钩子一样必须放置于动态链接库中,但是推荐其也在动态链接库中实现。因为这样的处理不仅可使钩子可为系统内的多个进程访问,也可以在系统中被直接调用,而且对于一个只供单进程访问的钩子,还可以将其钩子处理过程放在安装钩子的同一个线程内,此时SetWindowsHookEx()函数的第三个参数也就是该线程的实例句柄。

  在SetWindowsHookEx()函数完成对钩子的安装后,如果被监视的事件发生,系统马上会调用位于相应钩子链表开始处的钩子处理函数进行处理,每一个钩子处理函数在进行相应的处理时都要考虑是否需要把事件传递给下一个钩子处理函数。如果要传递,就通过函数CallNestHookEx()来解决。尽管如此,在实际使用时还是强烈推荐无论是否需要事件传递而都在过程的最后调用一次CallNextHookEx( )函数,否则将会引起一些无法预知的系统行为或是系统锁定。该函数将返回位于钩子链表中的下一个钩子处理过程的地址,至于具体的返回值类型则要视所设置的钩子类型而定。该函数的原型声明如下:

LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);

  其中,参数hhk为由SetWindowsHookEx()函数返回的当前钩子句柄;参数nCode为传给钩子过程的事件代码;参数wParam和lParam 则为传给钩子处理函数的参数值,其具体含义同设置的钩子类型有关。

  最后,由于安装钩子对系统的性能有一定的影响,所以在钩子使用完毕后应及时将其卸载以释放其所占资源。释放钩子的函数为UnhookWindowsHookEx(),该函数比较简单只有一个参数用于指定此前由SetWindowsHookEx()函数所返回的钩子句柄,原型声明如下:

BOOL UnhookWindowsHookEx(HHOOK hhk);

 鼠标钩子的简单示例

 

  最后,为更清楚展示HOOK技术在VC编程中的应用,给出一个有关鼠标钩子使用的简单示例。在钩子设置时采用的是全局钩子。下面就对鼠标钩子的安装、使用以及卸载等过程的实现进行讲述:

  由于本例程需要使用全局钩子,因此首先构造全局钩子的载体--动态链接库。考虑到 Win32 DLL与Win16 DLL存在的差别,在Win32环境下要在多个进程间共享数据,就必须采取一些措施将待共享的数据提取到一个独立的数据段,并通过def文件将其属性设置为读写共享:

#pragma data_seg("TestData")
HWND glhPrevTarWnd=NULL; // 窗口句柄
HWND glhHook=NULL; // 鼠标钩子句柄
HINSTANCE glhInstance=NULL; // DLL实例句柄
#pragma data_seg()
……
SECTIONS // def文件中将数据段TestData设置为读写共享
TestData READ WRITE SHARED

  在安装全局鼠标钩子时使用函数SetWindowsHookEx(),并设定鼠标钩子的处理函数为MouseProc(),安装函数返回的钩子句柄保存于变量glhHook中:

void StartHook(HWND hWnd)
{
……
glhHook=(HWND)SetWindowsHookEx(WH_MOUSE,MouseProc,glhInstance,0);
}

  鼠标钩子安装好后,在移动、点击鼠标时将会发出鼠标消息,这些消息均经过消息处理函数MouseProc()的拦截处理。在此,每当捕获到系统各线程发出的任何鼠标消息后首先获取当前鼠标所在位置下的窗口句柄,并进一步通过GetWindowText()函数获取到窗口标题。在处理函数完成后,通过CallNextHookEx()函数将事件传递到钩子列表中的下一个钩子处理函数:

LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam)
{
LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam;
if(nCode>=0)
{
HWND glhTargetWnd=pMouseHook->hwnd;
//取目标窗口句柄
HWND ParentWnd=glhTargetWnd;
while(ParentWnd !=NULL)
{
glhTargetWnd=ParentWnd;
//取应用程序主窗口句柄
ParentWnd=GetParent(glhTargetWnd);
}
if(glhTargetWnd!=glhPrevTarWnd)
{
char szCaption[100];
//取目标窗口标题
GetWindowText(glhTargetWnd,szCaption,100);
……
}
}
//继续传递消息
return CallNextHookEx((HHOOK)glhHook,nCode,wParam,lParam);
}

  最后,调用UnhookWindowsHookEx()函数完成对钩子的卸载:

void StopHook()
{
……
UnhookWindowsHookEx((HHOOK)glhHook);
}

  现在完成的是鼠标钩子的动态链接库,经过编译后需要经应用程序的调用才能实现对当前系统下各线程间鼠标消息的拦截处理。这部分同普通动态链接库的使用没有任何区别,在将其加载到进程后,首先调用动态链接库的StartHook()函数安装好钩子,此时即可对系统下的鼠标消息实施拦截处理,在动态链接库被卸载即终止鼠标钩子时通过动态链接库中的StopHook()函数卸载鼠标钩子。

  经上述编程,在安装好鼠标钩子后,鼠标在移动到系统任意窗口上时,马上就会通过对鼠标消息的拦截处理而获取到当前窗口的标题。实验证明此鼠标钩子的安装、使用和卸载过程是正确的。

  小结

  钩子,尤其是系统钩子具有相当强大的功能,通过这种技术可以对几乎所有的Windows系统消息和事件进行拦截处理。这种技术广泛应用于各种自动监控系统对进程外消息的监控处理。本文只对钩子的一些基本原理和一般的使用方法做了简要的探讨,感兴趣的读者完全可以在本文所述代码基础之上用类似的方法实现对诸如键盘钩子、外壳钩子等其他类型钩子的安装与使用。本文所述代码在Windows 98下由Microsoft Visual C++ 6.0编译通过

HOOK API是一个永恒的话题,如果没有HOOK,许多技术将很难实现,也许根本不能实现。
这里所说的API,是广义上的API,它包括DOS下的中断,WINDOWS里的API、中断服务、IFS和
NDIS过滤等。比如大家熟悉的即时翻译软件,就是靠HOOK TextOut()或ExtTextOut()这两个
函数实现的,在操作系统用这两个函数输出文本之前,就把相应的英文替换成中文而达到即
时翻译;IFS和NDIS过滤也是如此,在读写磁盘和收发数据之前,系统会调用第三方提供的
回调函数来判断操作是否可以放行,它与普通HOOK不同,它是操作系统允许的,由操作系统
提供接口来安装回调函数。
  甚至如果没有HOOK,就没有病毒,因为不管是DOS下的病毒或WINDOWS里的病毒,
都是靠HOOK系统服务来实现自己的功能的:DOS下的病毒靠HOOK INT 21来感染文件(文件型病毒),靠HOOK INT 13来感染引导扇区(引导型病毒);WINDOWS下的病毒靠HOOK 系统API(包括RING0层的和RING3层的),或者安装IFS(CIH病毒所用的方法)来感染文件。因此可以说“没有HOOK,就没有今天多姿多彩的软件世界”。

  由于涉及到专利和知识产权,或者是商业机密,微软一直不提倡大家HOOK它的系统API,
提供IFS和NDIS等其他过滤接口,也是为了适应杀毒软件和防火墙的需要才开放的。所以在
大多数时候,HOOK API要靠自己的力量来完成。

  HOOK API有一个原则,这个原则就是:被HOOK的API的原有功能不能受到任何影响。就象
医生救人,如果把病人身体里的病毒杀死了,病人也死了,那么这个“救人”就没有任何意义了。
如果你HOOK API之后,你的目的达到了,但API的原有功能失效了,这样不是HOOK,而是REPLACE,操作系统的正常功能就会受到影响,甚至会崩溃。

  HOOK API的技术,说起来也不复杂,就是改变程序流程的技术。在CPU的指令里,有几条
指令可以改变程序的流程:JMP,CALL,INT,RET,RETF,IRET等指令。理论上只要改变API入口和出口之间的任何机器码,都可以HOOK,但是实际实现起来要复杂很多,因为要处理好以下问题:
1,CPU指令长度问题,在32位系统里,一条JMP/CALL指令的长度是5个字节,因此你只有替换API
里超过5个字节长度的机器码(或者替换几条指令长度加起来是5字节的指令),否则会影响被更
改的小于5个字节的机器码后面的数条指令,甚至程序流程会被打乱,产生不可预料的后果;
2,参数问题,为了访问原API的参数,你要通过EBP或ESP来引用参数,因此你要非常清楚你的HOOK代码里此时的EBP/ESP的值是多少;
3,时机的问题,有些HOOK必须在API的开头,有些必须在API的尾部,比如HOOK CreateFilaA(),
如果你在API尾部HOOK API,那么此时你就不能写文件,甚至不能访问文件;HOOK RECV(),
如果你在API头HOOK,此时还没有收到数据,你就去查看RECV()的接收缓冲区,里面当然没有
你想要的数据,必须等RECV()正常执行后,在RECV()的尾部HOOK,此时去查看RECV()的缓冲区,
里面才有想要的数据;
4,上下文的问题,有些HOOK代码不能执行某些操作,否则会破坏原API的上下文,原API就失效了;
5,同步问题,在HOOK代码里尽量不使用全局变量,而使用局部变量,这样也是模块化程序的需要;
6,最后要注意的是,被替换的CPU指令的原有功能一定要在HOOK代码的某个地方模拟实现。


下面以ws2_32.dll里的send()为例子来说明如何HOOK这个函数:

Exported fn(): send - Ord:0013h
地址     机器码             汇编代码
:71A21AF4 55               push ebp //将被HOOK的机器码(第1种方法)
:71A21AF5 8BEC             mov ebp, esp //将被HOOK的机器码(第2种方法)
:71A21AF7 83EC10             sub esp, 00000010
:71A21AFA 56               push esi
:71A21AFB 57               push edi
:71A21AFC 33FF             xor edi, edi
:71A21AFE 813D1C20A371931CA271   cmp dword ptr [71A3201C], 71A21C93 //将被HOOK的机器码(第4种方法)
:71A21B08 0F84853D0000         je 71A25893
:71A21B0E 8D45F8             lea eax, dword ptr [ebp-08]
:71A21B11 50               push eax
:71A21B12 E869F7FFFF         call 71A21280
:71A21B17 3BC7             cmp eax, edi
:71A21B19 8945FC             mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000         jne 71A2AFE6
:71A21B22 FF7508             push [ebp+08]
:71A21B25 E826F7FFFF         call 71A21250
:71A21B2A 8BF0             mov esi, eax
:71A21B2C 3BF7             cmp esi, edi
:71A21B2E 0F84AB940000         je 71A2AFDF
:71A21B34 8B4510             mov eax, dword ptr [ebp+10]
:71A21B37 53               push ebx
:71A21B38 8D4DFC             lea ecx, dword ptr [ebp-04]
:71A21B3B 51               push ecx
:71A21B3C FF75F8             push [ebp-08]
:71A21B3F 8D4D08             lea ecx, dword ptr [ebp+08]
:71A21B42 57               push edi
:71A21B43 57               push edi
:71A21B44 FF7514             push [ebp+14]
:71A21B47 8945F0             mov dword ptr [ebp-10], eax
:71A21B4A 8B450C             mov eax, dword ptr [ebp+0C]
:71A21B4D 51               push ecx
:71A21B4E 6A01             push 00000001
:71A21B50 8D4DF0             lea ecx, dword ptr [ebp-10]
:71A21B53 51               push ecx
:71A21B54 FF7508             push [ebp+08]
:71A21B57 8945F4             mov dword ptr [ebp-0C], eax
:71A21B5A 8B460C             mov eax, dword ptr [esi+0C]
:71A21B5D FF5064             call [eax+64]
:71A21B60 8BCE             mov ecx, esi
:71A21B62 8BD8             mov ebx, eax
:71A21B64 E8C7F6FFFF         call 71A21230 //将被HOOK的机器码(第3种方法)
:71A21B69 3BDF             cmp ebx, edi
:71A21B6B 5B               pop ebx
:71A21B6C 0F855F940000         jne 71A2AFD1
:71A21B72 8B4508             mov eax, dword ptr [ebp+08]
:71A21B75 5F               pop edi
:71A21B76 5E               pop esi
:71A21B77 C9               leave
:71A21B78 C21000             ret 0010


下面用4种方法来HOOK这个API:

1,把API入口的第一条指令是PUSH EBP指令(机器码0x55)替换成INT 3(机器码0xcc),
然后用WINDOWS提供的调试函数来执行自己的代码,这中方法被SOFT ICE等DEBUGER广泛采用,
它就是通过BPX在相应的地方设一条INT 3指令来下断点的。但是不提倡用这种方法,因为它
会与WINDOWS或调试工具产生冲突,而汇编代码基本都要调试;

2,把第二条mov ebp,esp指令(机器码8BEC,2字节)替换为INT F0指令(机器码CDF0),
然后在IDT里设置一个中断门,指向我们的代码。我这里给出一个HOOK代码:

lea ebp,[esp+12] //模拟原指令mov ebp,esp的功能
pushfd         //保存现场
pushad         //保存现场

//在这里做你想做的事情

popad         //恢复现场
popfd         //恢复现场
iretd         //返回原指令的下一条指令继续执行原函数(71A21AF7地址处)

这种方法很好,但缺点是要在IDT设置一个中断门,也就是要进RING0。


3,更改CALL指令的相对地址(CALL分别在71A21B12、71A21B25、71A21B64,但前面2条CALL之前有一个条件
跳转指令,有可能不被执行到,因此我们要HOOK 71A21B64处的CALL指令)。为什么要找CALL指令下手?
因为它们都是5字节的指令,而且都是CALL指令,只要保持操作码0xE8不变,改变后面的相对地址就可以转
到我们的HOOK代码去执行了,在我们的HOOK代码后面再转到目标地址去执行。

假设我们的HOOK代码在71A20400处,那么我们把71A21B64处的CALL指令改为CALL 71A20400(原指令是这样的:CALL 71A21230)
而71A20400处的HOOK代码是这样的:

71A20400:
pushad

//在这里做你想做的事情

popad
jmp 71A21230   //跳转到原CALL指令的目标地址,原指令是这样的:call 71A21230

这种方法隐蔽性很好,但是比较难找这条5字节的CALL指令,计算相对地址也复杂。

4,替换71A21AFE地址上的cmp dword ptr [71A3201C], 71A21C93指令(机器码:813D1C20A371931CA271,10字节)成为
call 71A20400
nop
nop
nop
nop
nop
(机器码:E8 XX XX XX XX 90 90 90 90 90,10字节)

在71A20400的HOOK代码是:
pushad
mov edx,71A3201Ch           //模拟原指令cmp dword ptr [71A3201C], 71A21C93
cmp dword ptr [edx],71A21C93h   //模拟原指令cmp dword ptr [71A3201C], 71A21C93
pushfd

//在这里做你想做的事

popfd
popad
ret
这种方法隐蔽性最好,但不是每个API都有这样的指令,要具体情况具体操作。


  以上几种方法是常用的方法,值得一提的是很多人都是改API开头的5个字节,但是现在很多杀毒软件用这样的方法
检查API是否被HOOK,或其他病毒木马在你之后又改了前5个字节,这样就会互相覆盖,最后一个HOOK API的操作才是有效的,
所以提倡用第3和第4种方法。

                                                                              来自:http://www.yesky.com/31/1715031_1.shtml

                                                                                  http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=591933

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值