`
cfeers
  • 浏览: 141390 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Win32全局钩子在Delphi下实现的关键技术

阅读更多
<script type="text/javascript"></script>

自15年前Windows 3.1推出以来,Win32全局钩子的实现始终是32位Windows程序设计中最富挑战性的课题之一。全局钩子可以捕获系统向应用程序发送的消息(比如键盘和鼠标操作、系统设置改变等等),因而被广泛地应用在各种商用应用软件中,其中又以金山词霸的“屏幕取词”功能最为国人所熟知。另一方面,黑客们不断地开发出更隐蔽、更强大的钩子程序来盗取他人的密码和隐私。在互联网上我们可以轻易找到成百上千写法各异的Delphi 钩子程序,可到目前为止,它们中没有一个是可以同时正确运行于Windows9x/Me和NT/2000/XP下的,尤其是最常用的Windows XP下的全局钩子的例程,大多存在着各种各样的问题,最终导致它们或在运行时出错,或只能实现局部钩子的功能。

        本文将从Win32钩子的基础概念和Win32进程内存管理的特点讲解起,最终为读者提供一个完整的、可以运行在所有32位Windows平台下的全局鼠标钩子程序(一个TOEFL报名防盲抢计时器)。通过这个例子我们将会看到,在Win32全局钩子的实现中,对钩子本身的正确理解和使用只占到了大约一半的工作量。

        下面就让我们揭开钩子的神秘面纱。我们首先剖析钩子的工作机制,再讨论在Delphi 下的每一处实现方法及其必要性。

什么是钩子?钩子如何工作?

        Win32钩子的英文名称是Win32 Hooks,它是Windows操作系统消息驱动(Message Driven)机制的重要应用,也是Windows系统的重要特色。当Windows产生或收到用户操作的某一事件时,就会向应用程序发送一条消息(Message)。将会收到这条消息的应用程序被串在一条链(Chain)上。这条链是一个类似于队列(FIFO,First-In, First-Out)的数据结构,首先由排在队首的程序接收消息,然后逐个向后传递,直到队尾,这样就形成了消息处理的优先级。这种思想事实上是从 8086时代的中断优先级思想发展而来的,具有很强的实用性和合理性。

        现在我们希望能在消息到达应用程序之前,亦即在刚刚提到的链首之前,“挂”上一些“钩子”,来捕获、处理、甚至于截断系统消息。钩子就像是插队的应用程序,它和普通应用程序的不同之处在于:它可以在消息到达应用程序之前就捕获消息。这个特性也是钩子最有用的地方之一。设想,当用户在键盘上按下一个键时,我们可以让它在到达应用程序时变成另一个,或者干脆不让应用程序知道这回事。

        当然,钩子也可以挂不止一个,我们约定最后挂上去的钩子总在最前面(也就是第一个得到和处理系统消息)。

安装和卸载钩子的方法

        刚才我们提到了挂钩子,事实上应该说成安装钩子(Installing a hook)更加标准。在我们的程序或任务结束时,我们也应该“脱钩”,亦即卸载钩子(Unhook a hook)。这两项工作是通过调用API函数(API Call)来完成的,在Win32中,我们应该使用SetWindowsHookEx和UnhookWindowsHookEx。它们在我们的程序中的调用方法为:

HookHandle := SetWindowsHookEx(WH_MOUSE,MouseProc,hInstance,0);
UnhookWindowsHookEx(HookHandle);

        对于SetWindowsHookEx,我们需要提供钩子的类型(如WH_MOUSE)、钩子的过滤函数(Filter Function)的地址(如MouseProc)、调用钩子的应用程序实例句柄(Handle,如hInstance),以及钩子需要作用的范围的句柄(本例中的0表示全局)。

        首先我们来了解钩子的类型及其作用范围。不同类型的钩子所支持的作用范围是有所不同的,Windows一共提供了12类钩子,用来捕获12类不同的消息。具体的钩子及其类型约束请参阅MSDN中的《Win32 Hooks》。在本例中,我们使用的是WH_MOUSE类型的钩子,它用于捕获鼠标操作。它同时支持线程和全局范围作用,为了让其作用范围为全局,我们设置第四个参数为0即可。如果要让这个钩子作用于某个线程,我们则要提供线程句柄。

        安装钩子时会返回一个句柄,在卸载时用得上,卸载钩子的方法很简单,将句柄作为参数调用UnhookWindowsHookEx即可。

过滤函数(Filter Function)或回调函数(Callback Function)

        在安装钩子时,我们提供了钩子的所谓过滤函数(Filter Function)的地址。在本例中,我们提供的是MouseProc,这个函数是要我们自己编写的。我们要在这个函数中指定当我们的钩子接收到系统消息时要怎么做。由于这个函数通常是由Windows调用的,也被形象地称为回调函数(Callback Function)。有些参考书上提及这个函数的名称是固定的,事实上在Delphi 中并没有这个限制。

        本例中的MouseProc是这样写的:

function MouseProc(nCode:integer; wParam:WParam; lParam:LParam): LRESULT; stdcall;

        虽然函数的名称可以任取,但函数的原型(prototype)是固定的。在我们的钩子每一次被系统消息触发(trigger)时,我们同时得到的有 nCode,wParam和lParam这三个参数。其中nCode是一个整数,它包含有一些额外信息,不过我们只需要按下面的规则来处理它就可以了:如果nCode小于零,我们就原封不动地用CallNextHookEx来把参数传给下一个钩子。钩子链是由Windows管理的,其中的机制我们并不需要了解。wParam和lParam则包含了更多我们关心的的内容。在本例中,wParam包含了鼠标事件的具体类型,比如到底是左击还是右击,单击还是双击等等,有一系列常量与之对应,只要看看源码就不难理解。lParam是一个指针,它指向一个TMouseHookStruct,这个结构体中包含了鼠标的一系列当前状态,比如位置坐标信息。

引入动态链接库(Dynamic Link Library)必要性

        在本例中,由于我们要安装一个全局钩子,必须将和钩子有关的函数放在一个动态链接库(DLL,Dynamic Link Library)中。所谓动态链接库,就是在程序需要时可以被装入内存的函数库。通常我们把这个过程称为注入(Injection)。由于32位 Windows下的每个应用程序都拥有自己的地址空间,无法相互访问,如果我们把钩子放在EXE程序中就不能监控其它程序鼠标操作了,所以必须引入 DLL。

        顺带提及,使用不同于本文阐述的、更加复杂的方法,是可以不引入DLL而编写一个全局键盘钩子的。但编写全局鼠标钩子必须引入DLL,这也是鼠标操作更加安全的原因。在某些银行的网上支付系统中,要求用户在软键盘上用鼠标输入密码,就是这个原因。

内存映像文件(File Mapping)

        既然我们要把钩子放在DLL中,钩子和程序就变成了两个独立的线程(Thread),一个是DLL,一个是EXE。这时就不可避免地需要两个进程间通信的技术。在Windows 9x/Me下,我们可以通过声明一个共享数据段来共享数据;但在Windows NT/2000/XP环境下,我们必须使用所谓的内存映像文件(File Mapping)技术。

        内存映像文件技术隶属于Win32 API,它可以创建一块共享内存,供所有Windows应用程序访问。每一块共享内存都有一个文件名,不同的程序可以通过这个文件名来定位这块内存。这块共享内存实质上可以是一个文件系统中的文件,也可以是Windows页面文件(Page file)中的一块。

        在程序中我们编写了一个名为untMouseHookConst.pas的单元,其中的下列代码是值得说明的:

const MappingFileName='_TOEFLDllMouse';

type TSharedMem=record
InstHandle :DWord;
MessageID :DWord;
end;

        这个单元同时被DLL和EXE引用,其中的MappingFileName就是内存映像文件的文件名,而TSharedMem类型则是我们要创建的共享内存在实际存储时采用的结构体类型。InstHandle用于在程序开始时由EXE向DLL传递EXE主窗口的句柄,这样当DLL钩子收到系统消息时可以使用SendMessage函数向EXE回传消息,InstHandle就是用于定位EXE句柄。MessageID则是我们自定义的 SendMessage用户消息编号。

        具体的实现方法请参考源码及其注释,阅读时请特别注意内存映像文件产生和读取的时序关系。

进程间的通信:使用SendMessage发送消息

        创建内存映像文件的目的就是实现进程间的通信。在本例中,我们通过API函数SendMessage从DLL发送消息,在EXE中通过接管WndProc过程来处理消息。

        本例中SendMessage的调用语句为:

SendMessage(pSharedMem^.InstHandle,pSharedMem^.MessageID,0,0);

        为了能让接收端(即EXE)顺利地得到SendMessage所提供的参数,这里要求所有的参数都必须在内存映像文件中。参数中后面的两个0分别是 wParam和lParam,这里我们不用所以赋予0,它和过滤函数中的wParam和lParam是完全类似的。事实上,我们可以直接把过滤函数 wParam和lParam复制给SendMessage,当然必须保证数据都保存在内存映像文件中。之所以采用wParam和lParam这两个名称,是为了在形式上使得一系列API函数具有统一的接口,方便传递数据。从数据结构上看,wParam和lParam只是一般的整型变量而已。

        此外,MessageID是被定义为WM_User+121的常量,WM_USER表示此消息是用户消息,121是随机取的偏移量,一般在100-1024间选取都是可以的。

进程间的通信:接管WndProc以接收消息

        在接收端(即EXE),我们采用接管WndProc的方法来处理SendMessage发出的消息。由SendMessage发出的消息是同步到达EXE的,所以不必担心延迟问题,详见MSDN中的有关SDK文档。

        WndProc是窗口过程(Window Procedure)的缩写,它是应用程序真正开始动作的地方。前已述及,Windows程序是消息驱动的,每当收到一条消息程序就会执行 WndProc,在WndProc中进一步调用更多的函数和过程来完成更多操作。默认情况下,Delphi 会帮助程序员自动完成WndProc,但为了能处理自定义消息,我们需要使用override方法来接管WndProc,完成需要处理的特殊情况,再将其它的情况用Inherited方法继承回去。

        在EXE主窗口的单元中,我们需要把如下的语句放置在公用声明(Public Declaration)段:

procedure WndProc(var Messages:TMessage); override;

        这表明我们要接管WndProc。而在WndProc完成必要的工作后,必须加上Inherited语句。

例程:TOEFL报名防盲抢计时器

        TOEFL报名防盲抢(Blind Try)计时器在每一次用户单击鼠标左键后开始数秒,并用一个总在最前(Always On Top)的窗体显示于桌面上,这样用户就可以很清楚自己的操作是否达到了网站给出的最短间隔时间。

        这个程序中需要额外注意的一个问题是刚刚提到过的鼠标事件的具体类型,即过滤函数中的wParam参数。在我们判断时用到了两个事件:WM_LBUTTONUP和WM_NCLBUTTONUP。如果我们只判断前者,将会发现当我们单击屏幕上的标题栏、任务栏等区域时,计数器将不会重新计时。这是因为这些区域被称为非客户区(Non-client Area),单击这些区域的事件被定义为WM_NCLBUTTONUP。

DLL模块源代码:dllTOEFLHook.dpr

library dllTOEFLHook;

uses
SysUtils,
Windows,
untMouseHook in 'untMouseHook.pas',
untMouseHookConst in 'untMouseHookConst.pas';

exports
StartHook,StopHook;

begin
end.

DLL模块源代码:untMouseHook.pas

unit untMouseHook;

interface

uses Windows, Messages, Dialogs, SysUtils, untMouseHookConst;

var hMappingFile :THandle; //Handle for Mapping file
    pSharedMem   :^TSharedMem; //Pointer for Shared Memory
    HookHandle     :HHook; //Handle for the hook

function StartHook(Sender:HWnd; MessageID:word):BOOL; stdcall;
function StopHook:BOOL; stdcall;


implementation

function MouseProc(nCode:integer; wParam:WParam; lParam:LParam): LRESULT; stdcall;
begin
Result := 0;
if nCode<0 then Result:=CallNextHookEx(HookHandle,nCode,wParam,lParam);
//Rule of API call, which referred to Win32 Hooks topic in MSDN

if ( (wParam = WM_LBUTTONUP ) or ( wParam = WM_NCLBUTTONUP) ) then
    SendMessage(pSharedMem^.InstHandle,pSharedMem^.MessageID,0,0);
    //Sends Message to Instance to which was injected this DLL
end;

function StartHook(Sender:HWnd; MessageID:word):BOOL;
begin
Result := False;
if HookHandle<>0 then Exit; //Already Installed the hook
pSharedMem^.InstHandle := Sender;
pSharedMem^.MessageID := MessageID;
HookHandle := SetWindowsHookEx(WH_MOUSE,MouseProc,hInstance,0);
Result := HookHandle <> 0;
end;

function StopHook:BOOL;
begin
if HookHandle <> 0 then
begin
    UnhookWindowsHookEx(HookHandle);
    HookHandle := 0;
end;
    Result := HookHandle = 0;
end;


initialization

hMappingFile := OpenFileMapping(FILE_MAP_WRITE,False,MappingFileName);
//Try to open an existing mapping file as MappingFileName specified
if hMappingFile = 0 then //Not exist
    hMappingFile := CreateFileMapping($FFFFFFFF,nil,PAGE_READWRITE,0,sizeof(TSharedMem),MappingFileName);
    //Here $FFFFFFFF is a invalid file handle, which cause this file being created in Windows page file.

if hMappingFile = 0 then //Still unable to create a mapping file
    Exception.Create('Unable to create shared memory. Make sure your system have enough memory and page file space.');

pSharedMem := MapViewOfFile(hMappingFile,FILE_MAP_WRITE or FILE_MAP_READ,0,0,0);
//Details of this API call, refer to MapViewOfFile in MSDN
if pSharedMem = nil then //Create a pointer to the mapped file
begin
    CloseHandle(hMappingFile);
    Exception.Create('Unable to map shared memory. Program halt.');
end;

HookHandle := 0;
//Whether HookHandle = 0 is used to judge if this hooked was installed
//In function StartHook, we will later give a value to HookHandle


finalization

UnMapViewOfFile(pSharedMem);
CloseHandle(hMappingFile);


end.

公用模块源代码:untMouseHookConst.pas

unit untMouseHookConst;

interface

uses Windows;

const MappingFileName='_TOEFLDllMouse';

type TSharedMem=record
InstHandle :DWord;
MessageID :DWord;
end;

implementation

end.

EXE模块源代码:untMain.pas

unit untMain;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, untMouseHookConst, StdCtrls, ExtCtrls;

type
TfrmMain = class(TForm)
    lblMain: TLabel;
    tmrMain: TTimer;
    procedure tmrMainTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
private
    { Private declarations }
public
    { Public declarations }
    procedure WndProc(var Messages:TMessage); override;
end;

const MessageID = WM_User+121;
      DLLFileName = 'dllTOEFLHook.dll';

var
frmMain      :TfrmMain;
hMappingFile :THandle;
pSharedMem   :^TSharedMem;
time_counter :integer;

implementation

{ $R *.dfm }

function StartHook(Sender:HWnd; MessageID:word):BOOL; stdcall; external DLLFileName;
function StopHook:BOOL; stdcall; external DLLFileName;


procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if not StopHook then
    Exception.Create('Unable to uninstall the mouse hook. Abnormal termination.');
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
pSharedMem := nil;
if not StartHook(frmMain.Handle,MessageID) then //Sends handle and MessageID to DLL
    Exception.Create('Unable to install a mouse hook. Program halt.');
time_counter := 0;
end;

procedure TfrmMain.tmrMainTimer(Sender: TObject);
begin
inc(time_counter);
lblMain.Caption := IntToStr(time_counter);
end;

procedure TfrmMain.WndProc(var Messages: TMessage); //Override WndProc, see MSDN for details
begin
if pSharedMem = nil then
begin
    hMappingFile := OpenFileMapping(FILE_MAP_WRITE,False,MappingFileName);
    if hMappingFile = 0 then Exception.Create('Unable to access shared memory. Program halt.');
    pSharedMem := MapViewOfFile(hMappingFile,FILE_MAP_WRITE or FILE_MAP_READ,0,0,0);
    if pSharedMem = nil then
    begin
      CloseHandle(hMappingFile);
      Exception.Create('Unable to map to shared memory. Program halt.');
    end;
end;
if pSharedMem = nil then exit; //Halt program if unable to create/open/map shared memory.

if Messages.Msg = MessageID then //Global mouse On_Left_Button_Up
begin
    time_counter := 0;
    lblMain.Caption := 'CLICK!';
    tmrMain.Interval := 0;
    tmrMain.Interval := 1000; //Cause timer to restart timing
end
else Inherited; //Do traditional WndProc without override

end;

end.

结语

        程序在Delphi 7和Delphi 2006下均可编译通过。值得一提的是,本文只给出了全局鼠标钩子的一个简单实例。事实上,不同类型的钩子在实现中的差别还是比较大的,在编写不同类型的全局钩子时应当多参考不同参考书和互联网上的例程(虽然大多存在错误,但总是有参考价值的),并注意参阅MSDN中的解释并以此为准,毕竟MSDN才是最权威的Win32 API参考资料。

        Win32 Hooks编程是非常考验程序员的编程功底和技巧的,学习时一定要有花大量时间反复调试的准备,如果你遇到了任何问题或有任何发现,也欢迎和我交流、分享。请关注我的网站Wecan's Weblog: http://wecan.name/diary 。如果您转载本文,请不要修改这一信息。谢谢您的合作。

分享到:
评论

相关推荐

    delphi 写的全局钩子

    在实现全局钩子时,需要注意以下几点: 1. **钩子的生命周期**:钩子必须在一个线程中运行,该线程必须一直保持活动状态,否则钩子将失效。 2. **性能影响**:全局钩子会影响所有线程,如果处理不当,可能会对系统...

    Delphi实现全局鼠标钩子

    在编程领域,全局鼠标钩子是一种技术,允许开发者拦截并处理系统中的鼠标事件,无论这些事件发生在哪个应用程序中。在Delphi环境下,我们可以利用Windows API函数来实现这种功能。本篇文章将深入探讨如何使用Delphi...

    DELPHI编写的HOOK API实现DLL全局钩子启动记事本的程序-.rar

    这里主要介绍以下内容:DELPHI编程语言、HOOK技术、API调用、DLL动态链接库以及全局钩子的实现。 首先,DELPHI是一种基于Object Pascal的集成开发环境,由Borland公司(现Embarcadero Technologies)开发,用于创建...

    全局钩子终结进程

    在"ProcessKeeperDemo"这个示例中,很可能是实现了一个简单的程序,它首先安装一个全局钩子,然后在钩子处理函数内检查事件是否来自于目标进程。如果是,那么它就调用`TerminateProcess`函数来结束该进程。这为学习...

    枚举全局消息钩子的delphi代码

    标题中的“枚举全局消息钩子”是指在Delphi编程中实现的一种技术,它涉及到Windows API函数的应用,尤其是`SetWindowsHookEx`和`EnumWindows`。全局消息钩子允许程序监视系统中所有窗口的消息,而不仅仅是那些属于该...

    PDFView_Win32.rar_delphi PDF控件_delphi pdf_delphi 显示 pdf_pdf ocx

    PDFView_Win32.rar 是一个包含 Delphi 开发中使用的 PDF 控件的压缩包,主要组件为 PDFView.OCX。这个控件允许开发者在 Delphi 应用程序中集成 PDF 文档的查看功能。Delphi 是一种流行的 Object Pascal 编程环境,常...

    DelphiLibUSB-WIN32.rar_DelphiLibUSB-WIN32_USB delphi_USBLibExpor

    "DelphiLibUSB-WIN32"项目包含了一系列DELPHI环境下针对LIB USB的例程,这些例程展示了如何在DELPHI中导入并使用LIB USB库。开发者可以通过这些实例学习如何调用LIB USB的函数,如`libusb_init`来初始化USB库,`...

    HookMouseKey_Delphi.rar_delphi 键盘钩子_键盘鼠标钩子

    在Delphi中实现“键盘钩子”和“鼠标钩子”是一项高级技术,常用于系统监控、游戏外挂或辅助工具的开发。本压缩包文件“HookMouseKey_Delphi.rar”提供了这样的实现示例,特别是针对键盘鼠标记录和回放的功能。 ...

    win32 api加delphi操作

    本篇文章将深入探讨“win32 api加delphi操作”这一主题,结合提供的资源,如`WIN32API.chm`和`Delphi+Win32核心API参考_split_1.pdf`,我们将详细介绍如何在Delphi中使用Win32 API。 首先,Win32 API是Microsoft ...

    Win32 Hooks 钩子详细介绍

    Win32 Hooks 钩子详细介绍

    Delphi.Win32核心API参考

    在Delphi编程中,Win32 API(Application Programming Interface)是开发者调用操作系统功能的重要途径,它提供了丰富的函数和数据结构,使得开发者能够实现更底层的操作,如系统级交互、硬件控制以及高级图形绘制等...

    delphi写的全局鼠标钩子

    全局鼠标钩子是Windows编程中一个重要的技术,它允许应用程序拦截和处理系统中所有其他窗口的鼠标事件,而不仅仅是属于该应用程序的窗口。在Delphi编程环境中,通过使用Windows API函数,我们可以创建这样的全局鼠标...

    Win32键盘Hook的Delphi编程.pdf

    为了更好地理解如何使用Delphi实现Win32键盘Hook,我们可以通过一个简单的示例来进行讲解。 ##### 1. 定义回调函数 首先,我们需要定义一个回调函数来处理Hook捕获的键盘消息。回调函数通常需要遵循特定的格式: ...

    openssl动态链接库WIN32、WIN64[delphi indy idhttp dll]

    delphi indy idhttp控件访问https所需openssl动态链接库WIN32、WIN64; 32位程序复制使用WIN32文件夹下的动态链接库, 64位程序复制使用WIN64文件夹下动态链接库; 下载解压后文件目录如下: WIN32\libeay32.dll、ssleay...

    Delphi Win32核心API参考

    在《Delphi Win32核心API参考》中,不仅包含了上述知识点的详细解释,还配以实际的源代码示例,帮助读者更好地理解和应用这些API。通过深入学习,开发者可以编写出更高效、更稳定、更具系统级功能的Delphi应用程序。

    delphi的win32api类

    Delphi的Win32API类是一组用于在Delphi编程环境中访问Windows操作系统底层功能的组件。这些组件使得开发者能够利用Windows API(应用程序接口)提供的丰富功能,如系统控制、文件操作、用户界面交互等。在Delphi中,...

    Win32API(6.0) for Delphi

    Delphi language projection for Microsoft Win32 Metadata. Uses metadata from Microsoft Win32 Metadata project Supports Delphi 7 - 12 What's new Version 6.0 declarations generated from Windows....

    Win32各种API函数的Delphi单元

    Delphi,作为一款强大的面向对象的 Pascal 编程语言,允许开发者直接调用Win32 API来实现更为底层和高效的功能。 "Win32各种API函数的Delphi单元"通常是一些包含了大量预定义的Delphi接口,用于方便地调用Win32 API...

    DELPHI编写的HOOK API实现DLL全局钩子启动记事本的程序-DELPHI prepared HOOK API to a

    DELPHI编写的HOOK API实现DLL全局钩子启动记事本的程序-DELPHI prepared HOOK API to achieve the overall hook DLL procedures start Notepad

    设置全局鼠标钩子

    总的来说,通过这个示例,我们可以学习到如何在Delphi中创建一个全局鼠标钩子,从而实现对系统范围内的鼠标事件的监控。这在某些特定的软件开发场景中,如键盘鼠标监控、游戏外挂等,是非常有用的技巧。

Global site tag (gtag.js) - Google Analytics