关于Hook的概念,可以先参考:
微软中国社区-Hook专题
http://www.microsoft.com/china/community/program/originalarticles/techdoc/hook.mspx
MSDN SetWindowsHookEx
http://msdn.microsoft.com/en-us/library/ms644990(VS.85).aspx
以及Jeffrey的《Windows95程式设计指南》
这里推荐Hook专题,不过需要补充的是在Hook专题中把钩子分为线程钩子和系统钩子:
SetWindowsHookEx()函数的最后一个参数决定了此钩子是系统钩子还是线程钩子。
线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
系统勾子监视系统中的所有线程的事件消息。因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须放在独立的动态链接库(DLL) 中。系统自动将包含勾子回调函数的DLL映射到受钩子函数影响的所有进程的地址空间中,即将这个DLL注入了那些进程。
这种说法并不妥当,根据Jeffrey的指南:
钩子可以分为local hooks和remote hooks,local hooks所拦截的是原本预定要给“这个进程中设定这个hook的线程”的events,而remote hooks所拦截的则是原本要给“其它进程的线程”的events,local hooks一次仅能安装在一个线程上,而remote hooks有两种形式,一是针对特定线程(thread specific),一个是针对整个系统(system-wide),thread specific remote hooks所拦截的是“其它进程中的某个特定线程”的events,而system wide hooks所拦截的是“整个系统中所有进程空间中所有线程”的events。 如果你挂上的是一个remote hook,那么回调函数必须位于一个DLL之中,只有这样回调函数才能够被被操作系统注入到其它进程的地址空间中。 Windows提供了14种一同的hok类型,每一个都可以是local和remote,但WH_JOUNRNALRECORD和WH_JOURNALPLAYBACK两类hook除外,它们是system-wide local hooks,它们来会被注入到任何进程地址空间中,因此,journal hook的回调不一定得放在DLL中。 当你想挂的是一个system-wide hook,就请将dwThreadID设定为0,那么hook function就会拦截系统中所有进程的消息,当你相要挂上一个WH_JOURNALRECORD或是WH_JOURNALPLAYBACK或是WH_SYSMSGFILTER,你就必须指定dwThreadID为0。
换句话说,Hook专题中的线程钩子可以进一步分为本进程线程钩子和远程进程线程钩子,而系统钩子又可以分为本地系统钩子和远程系统钩子。其中远程进程线程钩子和远程系统钩子的回调用函数必须放在DLL中,且DLL会被系统用于注入。
在MSDN的SetWindowsHook中有一个表列出了每种钩子的适用范围,我结合Hook专题一文的内容添加了Desc,表如下:
Hook | Scope | Desc |
---|---|---|
WH_CALLWNDPROC | Thread or global | 使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。 |
WH_CALLWNDPROCRET | Thread or global | |
WH_CBT | Thread or global | 在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括: |
WH_DEBUG | Thread or global | 在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。 |
WH_FOREGROUNDIDLE | Thread or global | 当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程 |
WH_GETMESSAGE | Thread or global | 应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。 |
WH_JOURNALPLAYBACK | Global only | WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。 Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放 WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。 |
WH_JOURNALRECORD | Global only | WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。 |
WH_KEYBOARD | Thread or global | 在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。 |
WH_KEYBOARD_LL | Global only | WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。 |
WH_MOUSE | Thread or global | WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。 |
WH_MOUSE_LL | Global only | WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。 |
WH_MSGFILTER | Thread or global | WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。WH_SYSMSGFILTER Hook监视所有应用程序消息。 WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。 通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。 |
WH_SYSMSGFILTER | Global only | |
WH_SHELL | Thread or global | 外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。 |
LRESULT CALLBACK CallWndProc( int nCode, WPARAM wParam, LPARAM lParam );
KeyBoardHookStruct input = (KeyBoardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyBoardHookStruct));
这里推荐后一种方法
2、对于本地系统钩子如WH_JOURNALRECORD、WH_KEYBOARD_LL,可以在应用中直接挂接而不需要将回调用放在DLL中,但SetWindowsHookEx的第三个参数仍然需要传递非空的模块句柄:
API.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName) Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]) Marshal.GetHINSTANCE( Assembly.GetExecutingAssembly().ManifestModule )
这个句柄可以用以上三种代码中的任一种获取,但经测试只有第一种有效果,后两种在DLL中调用SetWindowsHookEx时有效(注意,这类钩子DLL不用于注入)
3、.net是托管代码,使用.net编译的dll是无法用于注入的,在《HOW TO:在 Visual C# .NET 中设置窗口挂钩》
http://support.microsoft.com/kb/318804
您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。
这里的“全局”挂钩应该就是指远程进程线程钩子和远程系统钩子。这两种钩子的回调必须放在DLL中,且该DLL会被系统用来注入,解决办法只有用C++一类的编译器作出相关DLL
注意:如果通过2中的方式来挂接,如WH_KEYBOARD,SetWindowsHook不会返回0,但无论回调在本应用还是在DLL中,如果挂远程系统勾子,则本进程的按键消息会触发回调用(在没有在其它进程中按下按键的前提下),对于其它进程中的按键消息,回调用不会响应,如果是挂远程进程线程钩子,如计算器(通过Spy++获得GUI线程ID),则计算器进程会出现“数据执行保护-为帮助保护您的计算机,Windows已经关闭了此程序”错误提示
4、.net中调用SetWindowsHookEx的关键点在于其第二个参数,回调函数指针,在C#是不支持函数指针的,解决办法就是使用委托
示例代码:
/// <summary> /// 委托实例 /// </summary> /// <remarks>不要试图省略这个变量而直接传递方法名或构造一个局部委托实例给SetWindowsHookEx,这将不定时的触发CallbackOnCollectedDelegate错误</remarks> private HookProc hookProc = new HookProc(this.MyHookProc); /// <summary> /// Hook句柄 /// </summary> private IntPtr hookId = IntPtr.Zero; /// <summary> /// 安装Hook /// </summary> private void button1_Click(object sender, EventArgs e) { hookId = API.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, API.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0); if (hookId == IntPtr.Zero) Trace.WriteLine(Marshal.GetLastWin32Error()); } /// <summary> /// 卸载Hook /// </summary> private void button2_Click(object sender, EventArgs e) { if (hookId != IntPtr.Zero) API.UnhookWindowsHookEx(hookId); } /// <summary> /// 回调定义(委托) /// </summary> private int MyHookProc(int nCode, IntPtr wParam, IntPtr lParam) { KBDLLHOOKSTRUCT input = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); Trace.WriteLine(input.vkCode); return API.CallNextHookEx(hookId, nCode, wParam, lParam); }
上述代码中将hookProc定义为一个类成员十分重要,下面两种代码都可能产生问题:
private void button1_Click(object sender, EventArgs e) { hookId = API.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, MyHookProc, API.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0); if (hookId == IntPtr.Zero) Trace.WriteLine(Marshal.GetLastWin32Error()); }
private void button1_Click(object sender, EventArgs e) { HookProc hookProc = new HookProc(MyHookProc); hookId = API.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, API.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName), 0); if (hookId == IntPtr.Zero) Trace.WriteLine(Marshal.GetLastWin32Error()); }
问题都在于委托实例的生存期,代码一直接向SetWindowsHookEx传递方法名,这里存在一个隐式转换,生产了一个临时委托变量,代码二定义了一个局部委托变量,并传递给SetWindowsHookEx,当代码离开button1_Click方法时,上述两个变量就都过了生存期,根据.net的机制,在某一时候这两个变量会被垃圾回收,当回调用再次被触发时,由于委托实例已经被回收,这时就会引起CallbackOnCollectedDelegate异常
可以封装一个HookHelper类,将回调作成一个public的event,然后由一个缺省回调用来调用
private IntPtr hookId; public event HookProc hookProc; private int DefHookProc(int nCode, IntPtr wParam, IntPtr lParam) { if (hookProc != null && hookProc(nCode, wParam, lParam) == 1) return 1; else return API.CallNextHookEx(hookId, wParam, lParam); }
这里要注意的是,如果想作一个通用的SetHook,如:
public IntPtr SetHook(HookType hookType, int threadID) { if (hookId != IntPtr.Zero) { //SetWindowsHookEx } else return IntPtr.Zero; }
因为SetWindowsHookEx的第三个参数-模块句柄根据钩子类型不同而不同,因此,在SetHook中还需要判断hookType,threadID也是如此。
还有一种作法是,不使用HookProc作为event的类型,而是针对特定钩子封装,比如针对WH_KEYBOARD_LL使用
public delegate int DegKeyEvent(KeyEventArgs e); public event DegKeyEvent keyEvent;
特定的委托作为evnet的类型,然后在DefHookProc中封装KeyEventArgs
参考资料:
C#+低级Windows API钩子拦截键盘输入
http://dev.yesky.com/msdn/435/2492435.shtml
C# 钩子_凤凰城
http://hi.baidu.com/pxcy/blog/item/fabfa60f46ffb6e4ab645779.html
纯C#钩子实现及应用
http://sharkoo.cnblogs.com/archive/2006/03/27/357878.html
C#简单游戏外挂制作
http://www.cnblogs.com/BlackFeather/archive/2009/09/04/1559985.html
C#中的公共勾子类
http://blog.youkuaiyun.com/xiao_d/archive/2006/04/09/656103.aspx
深入探讨.NET中的钩子技术
http://develop.csai.cn/dotnet/200705221018351722.htm
C#系统钩子的实现
http://blog.youkuaiyun.com/ayhome/archive/2007/09/15/1786784.aspx
两分钟用C#搭建IE BHO勾子
http://blog.youkuaiyun.com/jackiechen01/archive/2007/08/11/1738010.aspx