Windows API 教程(七) hook 钩子监听

茵蒂克丝

如何创建一个窗口

另外一个再录的 Windows SDK教程 里面有讲到快捷创建窗口的方式,不过这样的话要分好几个文件,感觉有点混所以这里就用原始的方式创建一个窗口。

那么,为什么讲到 hook(钩子)的时候要去创建窗口呢?其实这个问题说起来也不复杂,简单点说,按博主这样写不用写DLL也不用资源文件,实际上是把问题简化了一些。通常 hook 是用来监听自己窗口上的键盘和鼠标输入的,监听全局的通常是设置一些全局的热键(如QQ的 Ctrl+Alt+Z 调出QQ窗口),这些常见的功能也都是要依托窗口才能存在。所以我们先来简单说下手动建立一个窗口的流程。

手动创建窗口的流程

  1. 设置注册窗口结构体
  2. 使用【窗口结构体】注册窗口
  3. 创建窗口
  4. 显示窗口
  5. 窗口过程处理
  6. 消息循环

实际代码

这里不会详细讲这个,有感兴趣的可以去追博主的 SDK教程 或者去搜 杨中科的《C语言也能干大事》


#include <windows.h>

// 5. 窗口过程处理
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{	
	switch(msg)
	{
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;	// 更多详细都可以去百度的 http://baike.baidu.com/view/1750396.htm
    HWND hwnd;
    MSG Msg;
	char text[30];

	const char szClassName[] = "myWindowClass";

    // 1. 设置注册窗口结构体
    wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
    wc.style         = 0;								// 窗口的样式
    wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
    wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
    wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
    wc.hInstance     = hInstance;						// 本模块的实例句柄
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
    wc.lpszMenuName  = NULL;							// 指向菜单的指针
    wc.lpszClassName = szClassName;						// 指向类名称的指针
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标

	// 2. 使用【窗口结构体】注册窗口
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // 3. 创建窗口
    hwnd = CreateWindowEx(
		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
		szClassName,			// 指向注册类名的指针
		TEXT("窗口标题"),		// 指向窗口名称的指针
		WS_OVERLAPPEDWINDOW,	// 窗口风格
        CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
        NULL,					// 父窗口的句柄
		NULL,					// 菜单的句柄
		hInstance,				// 应用程序实例的句柄
		NULL					// 指向窗口的创建数据
		);

    if(hwnd == NULL)
    {
        MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

	// 4. 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // 6. 消息循环
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

因为是比较死的形式,以上代码大家混个脸熟,大概知道各个部分的作用就行了,博主也从来没有专门记过。

安装钩子 (Install hook)

简介

窗口建好了之后就要开始转到我们的正题了,首先我们需要明确的是,这个钩子(hook)到底是什么,那么博主这里也不做太书面的解释留下几个链接:

百度百科:hook
MSDN: Hooks
博客园: Beginning HOOK

各位可以多多参考,那么博主说下自己的理解:

windows 系统中的【hook 机制】,就类似于一个【消息过滤网】,如果我们向操作系统申请并成功对某个窗口安装了一个【hook】指定了【回调函数】,那么这个【回调函数】也就相当于我们人为对这个窗口添加了一个【消息过滤网】。此时当 windows 操作系统要对这个窗口发送任何消息的时候(例如按键、鼠标点击等消息)操作系统会先调用我们在【消息过滤网】中设置的【回调函数】去接受、处理、过滤等等,当然如果你在【回调函数】中拿到了数据却没有继续传递给窗口的话,就相当于拦截了这些消息。

打个简单的比方,如果你在系统全局安装了一个【键盘消息】的钩子,并且在其指定的【回调函数】中没有把这个键盘消息继续传递给系统上的窗口,那么你的所有【键盘消息】都被这个【hook】也就我们挂在这个【消息过滤网】上的【回调函数】给拦截了,这也意味着你的键盘会失灵。

SetWindowsHookEx 函数

那么 SetWindowsHookEx 函数就是我们用来在 windows 操作系统上安装钩子的函数,我们简单来看一下这个函数的原型:

HHOOK WINAPI SetWindowsHookEx(
  _In_  int idHook,			// 安装的钩子类型
  _In_  HOOKPROC lpfn,		// 处理消息的回调函数
  _In_  HINSTANCE hMod,		// 当前实例句柄
  _In_  DWORD dwThreadId	// 线程ID
);

钩子类型有很多种,本页中留的大部分链接上都有讲到这里就不废话了,关于 hMod(当前实例句柄)和 dwThreadId(线程ID)之间的一些小九九博主这里也不多说,各位可以到下方的链接中去看看,博主这里就举一个容易实现的实例。

百度百科: SetWindowsHookEx
MSDN: SetWindowsHookEx

设置监听【键盘】消息

PKBDLLHOOKSTRUCT 是 WH_KEYBOARD_LL 方式中用来接收消息的结构体,大家可以到 WindUser.h 中多逛逛。

我们监听键盘的时候主要用的是该结构体的 vkCode(value code)和 scanCode 这两个字段。即键盘的【值码】和【扫描码】那么为什么判断一个按键要分成两个部分呢,原因是因为世界上的键盘有很多种,不同国家、不同厂商生产的键盘甚,至同一个键盘上【同样的键】不同的地方按下都可能会有差异。vkCode 是常见的一般都是公用的键盘值,而 scanCode 扫描码则是用来辅助区分的一个一个参数,例如同样是按下 ctrl 键,他们的 vkCode 是相同的但是 scanCode 却不同。


#include <windows.h>

HHOOK myhook;	// 保存当前钩子句柄

/**************************************************************** 
  WH_KEYBOARD hook procedure 
  鍵盤钩子处理过程
 ****************************************************************/ 
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 
{ 	
	char text[50], data[20];	// 输出字符串
	const char *info = NULL;	// 类型字符指针
	PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam; // 获取按键消息
	HDC hdc;	// 画图设备句柄

	// 判断是否收到键盘消息
    if (nCode >= 0)
	{
		// 判断消息类型
		if 		(wParam == WM_KEYDOWN) 		info = "普通按鍵抬起";
		else if (wParam == WM_KEYUP) 		info = "普通按鍵按下";
		else if (wParam == WM_SYSKEYDOWN) 	info = "系統按鍵抬起";
		else if (wParam == WM_SYSKEYUP) 	info = "系統按鍵按下";

		// 初始化数组
		ZeroMemory(text, sizeof(text));
		ZeroMemory(data, sizeof(data));
		// 拼装字符串
		wsprintf(text, "%s - 键盘码 [%04d], 扫描码 [%04d]  ", info, p->vkCode, p->scanCode);
		wsprintf(data, "按鍵目测为: %c  ", p->vkCode);

		// 此处调用 GDI 画图函数来将截取到的内容画在窗口上
		hdc = GetDC(画图的窗口句柄);		// 获取要画图的设备句柄
		TextOut(hdc, 10, 10, text, strlen(text));	// 在窗口上画文字 
		TextOut(hdc, 10, 30, data, strlen(data));	// 参数分别是 目标设备, x坐标, y坐标, 字符串内容, 字符串长度
		ReleaseDC(画图的窗口句柄, hdc);		// 释放设备句柄
	}

    // 将消息继续往下传递
    return CallNextHookEx(myhook, nCode, wParam, lParam);
} 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
	/* 其他代码 */

	// 设置键盘全局监听
	myhook = SetWindowsHookEx( 
		WH_KEYBOARD_LL, // 监听类型【键盘消息】
		KeyboardProc,	// 处理函数
		hInstance,		// 当前实例句柄
		0				// 监听线程ID(NULL为全局监听)
	); 

	// 判断是否成功
	if(myhook == NULL)
	{		
		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
	}

	/* 其他代码 */
}

注:其中在输出按键的时候,直接用 %c 输出了 p->vkCode 部分,这个实际上不是很准确。

顺便提一句,各位也不要用这个功能去做什么坏事,比如去监听QQ窗口的键盘消息然后偷到密码盗别人号之类的, 博主已经试过了 这个本身有带防护的,用户在输密码的时候,会有干扰的键盘消息也一起冒出来所以没那么简单能到到别人输的密码。至于写个程序去拦截别人的键盘还有鼠标消息让别人的电脑不能用的情况,这个确实很容易做到,而且貌似杀毒软件都没办法防,只能自己在自己的电脑上留个后门,怎么写后门?后面的网络编程会说这个。

键盘监听完整代码:


#include <windows.h>

HWND hgWnd;
HHOOK myhook;

/**************************************************************** 
  WH_KEYBOARD hook procedure 
  鍵盤钩子处理过程
 ****************************************************************/ 
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 
{ 	
	PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
	const char *info = NULL;
	char text[50], data[20];

	PAINTSTRUCT ps;
	HDC hdc;

    if (nCode >= 0)
	{
		if      (wParam == WM_KEYDOWN)		info = "普通按鍵抬起";
		else if (wParam == WM_KEYUP)		info = "普通按鍵按下";
		else if (wParam == WM_SYSKEYDOWN)	info = "系統按鍵抬起";
		else if (wParam == WM_SYSKEYUP)		info = "系統按鍵按下";

		ZeroMemory(text, sizeof(text));
		ZeroMemory(data, sizeof(data));
		wsprintf(text, "%s - 键盘码 [%04d], 扫描码 [%04d]  ", info, p->vkCode, p->scanCode);
		wsprintf(data, "按鍵目測為: %c  ", p->vkCode);

		hdc = GetDC(hgWnd);			
		TextOut(hdc, 10, 10, text, strlen(text));
		TextOut(hdc, 10, 30, data, strlen(data));
		ReleaseDC(hgWnd,hdc);
	}
    
    return CallNextHookEx(myhook, nCode, wParam, lParam);
} 

// 5. 窗口过程处理
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{	
	hgWnd = hwnd;

	switch(msg)
	{
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;	// http://baike.baidu.com/view/1750396.htm
    HWND hwnd;
    MSG Msg;
	char text[30];

	const char szClassName[] = "myWindowClass";

    // 1. 设置注册窗口结构体
    wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
    wc.style         = 0;								// 窗口的样式
    wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
    wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
    wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
    wc.hInstance     = hInstance;						// 本模块的实例句柄
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
    wc.lpszMenuName  = NULL;							// 指向菜单的指针
    wc.lpszClassName = szClassName;						// 指向类名称的指针
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标

	// 2. 使用【窗口结构体】注册窗口
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // 3. 创建窗口
    hwnd = CreateWindowEx(
		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
		szClassName,			// 指向注册类名的指针
		TEXT("窗口标题"),		// 指向窗口名称的指针
		WS_OVERLAPPEDWINDOW,	// 窗口风格
        CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
        NULL,					// 父窗口的句柄
		NULL,					// 菜单的句柄
		hInstance,				// 应用程序实例的句柄
		NULL					// 指向窗口的创建数据
		);

    if(hwnd == NULL)
    {
        MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

	// 4. 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

	// 设置键盘全局监听
	myhook = SetWindowsHookEx( 
		WH_KEYBOARD_LL, // 监听类型【键盘】
		KeyboardProc,	// 处理函数
		hInstance,		// 当前实例句柄
		0				// 监听窗口句柄(NULL为全局监听)
	); 

	if(myhook == NULL)
	{		
		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
	}


    // 5. 消息循环
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

运行截图

键盘监听

设置监听【鼠标】消息

与键盘监听类似,各位直接看代码:

#include <windows.h>

HWND hgWnd;
HHOOK myhook;

/**************************************************************** 
  WH_KEYBOARD hook procedure 
  鍵盤钩子处理过程
 ****************************************************************/ 
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) 
{ 	
	LPMSLLHOOKSTRUCT p = (LPMSLLHOOKSTRUCT)lParam;
	POINT   pt = p->pt;
	DWORD   mouseData = p->mouseData;
	const char *info = NULL;
	char text[60], pData[50], mData[50];

	PAINTSTRUCT ps;
	HDC hdc;

    if (nCode >= 0)
	{
		if   (wParam == WM_MOUSEMOVE)		info = "鼠标移动    ";
		else if(wParam == WM_LBUTTONDOWN)	info = "鼠标【左键】按下";
		else if(wParam == WM_LBUTTONUP)		info = "鼠标【左键】抬起";
		else if(wParam == WM_LBUTTONDBLCLK)	info = "鼠标【左键】双击";
		else if(wParam == WM_RBUTTONDOWN)	info = "鼠标【右键】按下";
		else if(wParam == WM_RBUTTONUP)		info = "鼠标【右键】抬起";
		else if(wParam == WM_RBUTTONDBLCLK)	info = "鼠标【右键】双击";
		else if(wParam == WM_MBUTTONDOWN)	info = "鼠标【滚轮】按下";
		else if(wParam == WM_MBUTTONUP)		info = "鼠标【滚轮】抬起";
		else if(wParam == WM_MBUTTONDBLCLK)	info = "鼠标【滚轮】双击";
		else if(wParam == WM_MOUSEWHEEL)	info = "鼠标【滚轮】滚动";

		ZeroMemory(text, sizeof(text));
		ZeroMemory(pData, sizeof(pData));
		ZeroMemory(mData, sizeof(mData));

		wsprintf( text, "当前状态: %10s   ", info);
		wsprintf(pData, "0x%x - X: [%04d], Y: [%04d]  ", wParam, pt.x, pt.y);
		wsprintf(mData, "附带数据: %16u   ", mouseData);

		hdc = GetDC(hgWnd);			
		TextOut(hdc, 10, 10,  text, strlen(text));
		TextOut(hdc, 10, 30, pData, strlen(pData));
		TextOut(hdc, 10, 50, mData, strlen(mData));
		ReleaseDC(hgWnd,hdc);
	}
    
    return CallNextHookEx(myhook, nCode, wParam, lParam);
} 

// 5. 窗口过程处理
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{	
	hgWnd = hwnd;

	switch(msg)
	{
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;	// http://baike.baidu.com/view/1750396.htm
    HWND hwnd;
    MSG Msg;
	char text[30];

	const char szClassName[] = "myWindowClass";

    // 1. 设置注册窗口结构体
    wc.cbSize        = sizeof(WNDCLASSEX);				// 注册窗口结构体的大小
    wc.style         = 0;								// 窗口的样式
    wc.lpfnWndProc   = WndProc;							// 指向窗口处理过程的函数指针
    wc.cbClsExtra    = 0;								// 指定紧跟在窗口类结构后的附加字节数
    wc.cbWndExtra    = 0;								// 指定紧跟在窗口事例后的附加字节数
    wc.hInstance     = hInstance;						// 本模块的实例句柄
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);	// 图标的句柄
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);		// 光标的句柄
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);		// 背景画刷的句柄
    wc.lpszMenuName  = NULL;							// 指向菜单的指针
    wc.lpszClassName = szClassName;						// 指向类名称的指针
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);	// 和窗口类关联的小图标

	// 2. 使用【窗口结构体】注册窗口
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // 3. 创建窗口
    hwnd = CreateWindowEx(
		WS_EX_CLIENTEDGE,		// 窗口的扩展风格
		szClassName,			// 指向注册类名的指针
		TEXT("窗口标题"),		// 指向窗口名称的指针
		WS_OVERLAPPEDWINDOW,	// 窗口风格
        CW_USEDEFAULT, CW_USEDEFAULT, 350, 200, // 窗口的 x,y 坐标以及宽高
        NULL,					// 父窗口的句柄
		NULL,					// 菜单的句柄
		hInstance,				// 应用程序实例的句柄
		NULL					// 指向窗口的创建数据
		);

    if(hwnd == NULL)
    {
        MessageBox(NULL, TEXT("窗口创建失败"), TEXT("错误"),MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

	// 4. 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

	// 设置鼠标全局监听
	myhook = SetWindowsHookEx( 
		WH_MOUSE_LL,	// 监听类型【鼠标】
		MouseProc,	// 处理函数
		hInstance,		// 当前实例句柄
		0				// 监听窗口句柄(NULL为全局监听)
	); 

	if(myhook == NULL)
	{		
		wsprintf(text, "键盘监听失败!error : %d n", GetLastError());
		MessageBox(hwnd, text, TEXT("错误"), MB_OK);
	}


    // 5. 消息循环
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

MouseHook

上一讲: Windows API 教程(六) 动态链接库
下一讲: Windows API 教程(八) 注册快捷键

Advertisements

3 thoughts on “Windows API 教程(七) hook 钩子监听

  1. 在vc 6.0 中,【键盘监听完整代码】,得在#include 前面加上,#define _WIN32_WINNT 0x0500

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google+ photo

You are commenting using your Google+ account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s