Hook

关于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子程,这些事件包括:
1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
2. 完成系统指令;
3. 来自系统消息队列中的移动鼠标,键盘事件;
4. 设置输入焦点事件;
5. 同步系统消息队列事件。
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子程。
WH_SHELL 共有5种情況:
1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
2. 当Taskbar需要重画某个按钮;
3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
4. 当目前的键盘布局状态改变;
5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。
按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfo function注册它自己。

 
根据之前的描述,上表中标识为“Global only”的hook应该为本地系统钩子,也就是说它们的回调用不一定放在DLL中,换句话说,即使放入了DLL中,该DLL也不会被用来注入。
 
Hook API及其它相关定义的C#形式何以在pinvoke.net上找到,这里要注意几点:
 
1、pinvoke.net上关于SetWindowsHookEx有三种定义,看一下MSDN中的SetWindowsHookEx,会发现任一种钩子的回调函数原型都是相同的:
LRESULT CALLBACK CallWndProc(          
    int nCode,
    WPARAM wParam,
    LPARAM lParam
);
nCode、wParam、lParam的值根据钩子类型不同而不同(具体情况请参考MSDN或《95》),并且在部分钩子类型中,lParam指向的是一个钩子相关的数据结构,比如,对于WH_CBT钩子,lParam指向一个DEBUGHOOKINFO结构,而对于WH_KEYBOARD_LL钩子,lParam指向一个KBDLLHOOKSTRUCT结构,因此在pinvoke.net上就有了一种以上的原型定义,以方便调用。
在调用时,可以直接使用扩展的原型,也可以使用基本原型,然后在回调用中使用Marshal.PtrToStructure将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.csdn.net/xiao_d/archive/2006/04/09/656103.aspx

深入探讨.NET中的钩子技术

http://develop.csai.cn/dotnet/200705221018351722.htm

C#系统钩子的实现

http://blog.csdn.net/ayhome/archive/2007/09/15/1786784.aspx

两分钟用C#搭建IE BHO勾子

http://blog.csdn.net/jackiechen01/archive/2007/08/11/1738010.aspx

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值