“钩子是Windows的消息监视点,应用程序可以在这里安装一个监视子程序,这样就可以在系统中的消息流到达目的窗口过程前监控它们“
上面就是WIN32API手册中对钩子的描述。大概就是说钩子可以用来截获系统的消息。那么,要写一个钩子程序,肯定要先确定钩子的类型,钩子是有很多类型的,不同类型的钩子可以监视不同类型的消息。
钩子名称 | 监视消息的类型和时机 |
WH_CALLWNDPROC | 系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程序 |
WH_CALLWNDPROCRET | 窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程序 |
WH_GETMESSAGE | 应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。函数从程序的消息队列获取一个消息后调用钩子函数。 |
WH_KEYBOARD | 每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是WM_KEYUP或WM_KEYDOWN消息,则调用钩子函数。也就是说这个钩子可以监视键盘消息。 |
WH_MOUSE | WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息 |
WH_HEADWARE | 每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是鼠标消息,则调用钩子函数。就是说这个钩子用来监视鼠标的消息。 |
WH_MSGFILTER | 当用户对对话框,滚动条和菜单有所操作的时候,系统在发送对应的消息之前调用钩子函数,这种钩子只能是局部的 |
WH_SYSMSGFILTER | 和WH_MSGFILTER一样,但是是系统范围的 |
WH_SHELL | 当Windows shell程序准备接收一些通知事件前调用钩子函数,如shell被激活和重画等 |
WH_DEBUG | 用来给其他钩子函数除错 |
WH_CBT | 当基于计算机的训练(CBT)事件发生时调用钩子函数 |
WH_JOURNALRECORD | 日志记录钩子,用来记录发送给系统消息队列的所有消息 |
WH_JOURNALPLAYBACK | 日志回放钩子,用来回放日志记录钩子的系统事件 |
WH_FOREGROUNDLDLE | 系统空闲钩子,当系统空闲的时候调用钩子函数,这样就可以在这里安排一些优先级很低的任务 |
钩子分为局部钩子和远程钩子,局部钩子只能属于自身进程的事件,而远程钩子的除了可以钩到自己的还可以钩到其他进程里面的消息,远程的钩子又分为两种,基于线程的和系统范围的。基于线程的远程钩子用来捕获其他进程中某一特定的事件,而系统范围的远程钩子可以捕捉系统中所有进程中发生的事件消息。
局部的和远程的钩子还有一个不同的地方就是局部的钩子的钩子函数可以安装在进程中,而远程的钩子的钩子函数自能安装在DLL中。因为远程钩子要获得其他进程的消息,如果安装在本进程中,又因为进程之间的地址空间是隔离开来的,这样钩子就无法监视其他的进程的消息了。而DLL是可以插入到其他进程的地址空间中去的,所以远程钩子一般放在DLL中。但是也有两个例外,分别是日志记录钩子和日志回放钩子。这两个明明是远程钩子,但是它们的钩子函数可以安装在进程里面,并且不碍事。可能是因为这两个钩子函数要监视一些比较底层的消息,所以函数的调用可能是从Windows内部发起的。
那么,了解了一些钩子的基本概念之后,我们就可以开始准备写钩子程序了。所以来看一下一般的钩子程序的结构:
- 主程序:用来实现界面或者其他功能
- 钩子回调函数:用来接收系统发过来的消息
- 钩子的安装和卸载模块
对于局部钩子来说,这三个模块可以都在一个可执行文件中。但是对于远程钩子来说,第二部分必须放在DLL中。
先来看一下DLL的汇编源代码:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Include
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
hInstance dd ? ;动态链接库模块实例句柄
.data?
hWnd dd ? ;接收消息的窗口的句柄
hHook dd ? ;钩子句柄
dwMessage dd ? ;要发送的消息的类型
szAscii db 4 dup (?) ;按键的ASCII码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
DllEntry proc _hInstance,_dwReason,_dwReserved ;DLL入口
push _hInstance
pop hInstance
mov eax,TRUE
ret
DllEntry endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;键盘钩子回调函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
HookProc proc _dwCode,_wParam,_lParam ;消息的处理方式,按键的虚拟码,按键的其他信息
local @szKeyState[256]:byte
invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam ;调用下一个钩子的函数
invoke GetKeyboardState,addr @szKeyState ;获取键盘所有按键的当前状态
invoke GetKeyState,VK_SHIFT ;获取SHIFT键的当前状态
mov @szKeyState + VK_SHIFT,al
mov ecx,_lParam
shr ecx,16
invoke ToAscii,_wParam,ecx,addr @szKeyState,addr szAscii,0 ;将按键信息转换为ASCII码,并返回szAscii缓冲区中的字符的数量
mov byte ptr szAscii [eax],0 ;在缓冲区末尾加上字符个数
invoke SendMessage,hWnd,dwMessage,dword ptr szAscii,NULL
xor eax,eax ;返回0,表示将消息转发给目标窗口(不是我们自己的窗口)
ret
HookProc endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;安装钩子
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
InstallHook proc _hWnd,_dwMessage ;自己的窗口的句柄,自定义的消息类型
push _hWnd
pop hWnd
push _dwMessage
pop dwMessage
invoke SetWindowsHookEx,WH_KEYBOARD,addr HookProc,\ ;安装钩子函数,钩子类型,回调函数,DLL句柄,监控所有进程
hInstance,NULL
mov hHook,eax
ret
InstallHook endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;卸载钩子
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
End DllEntry
可以看到我们把安装和卸载钩子的模块也放到了钩子回调函数的模块里面。因为钩子句柄在钩子回调函数和卸载钩子的时候都要用到,而钩子句柄是在安装钩子的时候得到的,为了方便就把它们放一个模块了。
SetWindowsHookEx:
invoke SetWindowsHookEx,idHook,lpHookProc,hInstance,dwThreadID
.if eax
mov hHook,eax
.endif
该函数安装一个钩子。第一个参数指出钩子类型,第二个参数指出钩子回调函数的地址,第三个参数指出DLL的实例句柄,第四个参数指出钩子想监视的线程ID,ID可以指定为本进程的线程ID和其他进程的线程ID。如果这个参数为NULL,那么这个钩子会被解释成系统范围的,可以用来监视所有的进程以及它们的线程。
钩子安装成功返回一个钩子的句柄。
UnhookWindowsHookEx:
invoke UnhookWindowsHookEx,hHook
该函数卸载钩子,参数是钩子的句柄。
接下来就是最重要的钩子回调函数了。键盘钩子回调函数的一般结构是这样写的:
HookProc proc dwCode,wParam,lParam
invoke CallNextHookEx,hHook,_dwCode,_wParam,_lParam
;处理消息的代码
mov eax,返回值
ret
HookProc endp
一般是这么写的,当然只要能实现功能,不这么写也行。但是函数的格式是有要求的的,必须得是三个参数。三个参数的含义如下:
- dwCode:键盘消息的处理方式。如果是HC_ACTION,表示收到一个正常的击键消息,如果是HC_MOREMOVE,表示对应消息并没有从消息队列中移去。
- wParam: 按键的虚拟码
- lParam:按键的重复次数,扫描码和标志等数据,不同数据位的定义如下:
- 0~15位:按键的重复次数。
- 16~23位:按键的扫描码。
- 24位:按键是否是扩展键(如:F1和F2等,还有小数字键盘等),如果此位是1表示按键时扩展键。
- 25~28:未定义。
- 29位:如果Alt键在按下状态,此位置为1,否则为0。
- 30位:按键的原先状态,消息发送前按键原来是按下的,此键为1,否则为0。
- 31位:按键当前的动作,如果是按键按下,那么此位被设置为0,按键释放的话被设置为1。
然后这里还要介绍一个很重要的概念就是:钩子链。
WIndows中可以存在多个同种类型的钩子。这些钩子组成一个钩子链,最近加入的钩子放在链表的头部,Windows负责为每种钩子维护一个钩子链。当一个事件发生的时候,WIndows调用最后安装的钩子,然后由这个钩子的回调函数发起调用下一个钩子的动作,Windows收到这个动作后后,再从链表中取出下一个钩子的地址并将调用传递下去。
大概就是说同种类型的钩子在一条链子上,然后消息会依次从最后面那个钩子开始向前传,但这个动作需要我们在回调函数中完成。因为你不知道你的钩子是什么时候被放到钩子链里面的,所以我们最好还是把消息传递下去。
CallNextHookEx:
invoke CallNextHookEx,hHook,dwCode,wParam,lParam
这个函数就是调用下一个钩子的函数,第一个参数是当前钩子的句柄,后面三个参数就是当前钩子回调函数的三个参数。
我们写的这个是一个键盘钩子,所以我们要对获取的键盘消息先进行一个处理,然后再发给我们的主程序。先调用GetKeyboardState函数获取当前键盘的状态,然后因为我们获取的消息是按键的虚拟码和扫描码,我们要显示字符就得先把这些转换为ASCII码。ToAscii函数可以完成这个工作。最后我们再调用SendMessage函数将转换后的按键信息发送给我们的主窗口,主窗口的句柄是在主窗口调用DLL中的钩子安装程序的时候传递到DLL中的。消息类型是我们自己定义的。
然后钩子的DLL就写完了,但是我们在编译链接DLL的时候要注意一个地方就是,链接选项要指定是共享数据段的。所以链接的选项要使用 /section:.bss,S
做完这些后就只剩下主窗口的程序了。主窗口程序的结构就比较简单了,初始化的时候调用一下钩子安装函数,然后等待钩子消息来,有消息就显示到窗口,退出的时候就调用钩子卸载函数。
下面是主窗口程序的资源文件:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 0x1000
#define DLG_MAIN 0x1000
#define IDC_TEXT 0x1001
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 208,130,234,167
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "键盘钩子"
FONT 9, "宋体"
BEGIN
EDITTEXT IDC_TEXT,5,5,224,158,ES_MULTILINE | ES_AUTOVSCROLL | WS_BORDER | WS_VSCROLL | WS_TABSTOP | ES_READONLY
END
下面是汇编源代码:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Include??t?¨?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include Hookdll.inc
includelib Hookdll.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Equ
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN equ 1000h
DLG_MAIN equ 1000h
IDC_TEXT equ 1001h
WM_HOOK equ WM_USER + 100h
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;′ú???
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
local @dwTemp
mov eax,wMsg
.if eax == WM_CLOSE
invoke UninstallHook
invoke EndDialog,hWnd,NULL
.elseif eax == WM_INITDIALOG
invoke InstallHook,hWnd,WM_HOOK
.if ! eax
invoke EndDialog,hWnd,NULL
.endif
.elseif eax == WM_HOOK
mov eax,wParam
.if al == 0dh
mov eax,0a0dh
.endif
mov @dwTemp,eax
invoke SendDlgItemMessage,hWnd,IDC_TEXT,\
EM_REPLACESEL,0,addr @dwTemp
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam,eax,DLG_MAIN,NULL,\
offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
编译链接出来后运行:
然后我们随便在什么地方输入键盘:
除了一些特殊的字符显示不出来,勉强还是能看的。