Windows核心编程_Hook


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

一、前言


    Hook是Windows下的一个机制,Hook的中文意思是钩子的意思,顾名思义,钩子就是用来钩东西的,就好像钓鱼一样,你把鱼钩放入鱼塘里,钓到了某条鱼,即便我们不把鱼钓上来,我们可以通过鱼钩知道鱼在做什么,比如鱼飞速游动,鱼钩上的鱼线会做出反应,或者鱼原地不动,我们都可以通过鱼钩知道鱼在做什么!


    Windows就像一个鱼塘,而程序,就是鱼塘里的鱼,而用来监视这些鱼的鱼钩就是Hook!


    众所周知,Windows平台上的程序是以事件驱动和消息为基础工作的,事件与消息是关联的,消息的触发来响应事件,比如我单击了一个应用程序上的按钮,那么此时这个应用程序会触发一个消息即为MK_LBUTTON消息被发送到系统的消息队列里(触发过程是由操作系统根据鼠标点击某个窗口上某个控件来触发的,原理上是由操作系统触发的消息),这些消息都是以数据结构形式存储的,每个队列里不仅存储消息还有触发的窗口句柄等参数信息,Windows会把这些消息在转发给指定窗口进程下的消息队列,然后由消息队列来处理或者由操作系统来默认处理,学过Win32编程和MFC编程的应该比较熟悉,这里的消息流程只是仅限于Windows提供的WindowsSDK库下的API接口开发的程序!

image.png


    就好像控件一样,控件其实就相当于一个窗口,只是一个需要容器的窗口,也有自己的消息循环,学过COM组件开发的都知道基于COM组件开发ActiveX控件里也有一套消息循环机制和对应的响应事件,比如按钮的获取焦点对应的绘图函数!

 


    其它框架不一定使用此方法,比如QT的信号和槽,但QT内部封装了Windows消息和事件驱动模型,原理上来说,你单击QT上的一个按钮还是会产生MK_LBUTTON消息并返回给对应的QT程序,还是会返回到程序的消息循环队列里去,只是QT用了信号和槽的方式来代替消息与事件,比如你用第三方软件给QT程序上的某个按钮发送MK_LBUTTON消息还是会响应对应的事件,因为QT上的控件都是基于Windows下的SDK接口来实现完成的,QT的核心还是:触发消息>系统消息>应用消息,和Windows一样,只是内部封装起来了!


 

二 . 钩子运行机制

1.1 钩子链表与钩子


    钩子是Windows平台下的一个用于截获消息的程序段,Windows内部也在用它,当我们使用特定函数来安装一个钩子时,操作系统会给这个钩子分配一个钩子链表,并且这个钩子就相当于一小段程序,称为钩子子程序,这段程序会根据钩子类型的不同,来实现不同程度的消息截获,并且这个钩子链表里包含了这个钩子程序的地址,类型,回调函数的地址!


    并且钩子子程序的优先级会高于应用程序,在接受消息时会被钩子子程序先行截获,操作系统会先把消息发送给钩子,由钩子决定这些消息是否发送下去,钩子可以拦截这些消息,可以决定这些消息的作用,甚至可以屏蔽这些消息不让传递到指定应用程序当中!


    上面说过了每个程序上的钩子是由一个钩子链表来维护的,多个钩子那么链表上就会有多个子节点,当操作系统把消息传递给钩子时,会首先传递给链表首节点的钩子子程序,首节点的钩子可以决定这个消息是否传递给下一个钩子!


在安装钩子的时候是由顺序之分的,链表遵循的是先进后出,也就是说钩子链表的首节点始终是最后一个安装钩子的钩子子程序,并且每个进程下只能有一个钩子,如果重复安装钩子会安装失败!


1.2 钩子类型


1.1 全局钩子


    全局钩子即用于钩系统消息队列里的消息,上面说过,所有程序触发的消息都会被操作系统发送到消息队列里(这样做的原因是根据鼠标点击屏幕像素点区域来确定点击的是哪个窗口,窗口上的哪个控件),在由操作系统将消息队列里的消息发送给指定窗口下的消息队列,全局钩子的作用就是钩系统级的消息队列,当操作系统将消息队列里的消息发送出去时会率先发送给全局钩子,并且由全局钩子截获,全局钩子决定这些消息的存亡,钩子可以决定处理或不处理,也可以决定处理完之后再发送给应用程序或者不处理直接发送给应用程序,不过这样做的话钩子的意义就不大了!


 


1.2 局部钩子

    局部钩子即只钩进程下的消息,当操作系统在将消息发送给各个程序时,操作系统不会把所有消息都发送给此钩子,而是只把当前进程下产生的消息在发送给进程下的消息队列时先发送给钩子子程序,而不是直接发给消息队列,由钩子子程序决定这些消息的处理方式!


  1.3 HOOK应用模式

观察模式:

最常用的应用模式,即简单创建一个Hook子程序,用于观察某些进程下的消息,并对其进行截获处理!


         注入模式:


即通过Hook子程序将DLL动态库注入到某个进程下,使其成为进程的一部分!

        替换模式(注入模式的一种):


利用Hook子程序将动态库注入到某个进程下,并拦截某个进程调用函数过程,将调用函数替换成自己DLL动态库函数!(黑客常用)


         插件模式(注入模式的一种):


将动态库函数注入到指定进程下,并协调调用动态库函数,扩展程序业务!



 修复模式(替换模式的一种):


利用Hook技术将某个消息对应的函数替换成新的执行函数!

 


其还有其他应用模式,这一般取决于用户怎么使用Hook,用它做些什么!


 


1.4 Hook的运行机制-续


    上面说过Hook的工作机制,Hook的子程序的生存周期是由用户而决定的,当我们安装Hook子程序之后,就意味着操作系统要建立一张钩子链表来维护它,并且每次传递消息都要率先传递给钩子子程序,如果钩子子程序没有做任何处理则再由钩子子程序发送给对应的进程,那么这样来来回回,就需要浪费了很多时间,耗费系统资源,所以当在使用完钩子之后建议立马卸载,避免影响系统运行效率,并且如果你安装了钩子,但是在程序结束时钩子都没有被卸载,那么操作系统会帮你卸载这些钩子!


 

其钩子链表,这里要说一下,倘若多个进程多个钩子,其这些钩子链表是由操作系统来维护的,操作系统会生成一张系统用的钩子链表,这个链表负责存储每个钩子的相关信息!


 

    Hook的缺点非常明显,那就是当某个程序没有触发任何窗体消息时那么Hook永远不会被执行,其Windows下用于绘制窗口和处理事件驱动的模块是user32.dll,也就是说当一个窗口被创建和事件驱动等消息机制被创建出来时就会加载user32动态库,调用里面的API来完成,倘若某个进程在进行一些复杂的计算公式,不去调用user32那么Hook(仅限局部,因为不是所有的进程都会这样)将永远不会被执行!


 


三 . 实践

到了这一章相比各位的理论知识已经很足了,那么就可以开始进行实践了,毕竟实践来自于理论!


开始之前先介绍几个所需AIP函数:


1.SetWindowsHookEx

函数原型:
WINUSERAPI HHOOK WINAPI SetWindowsHookExW(    _In_ int idHook,    _In_ HOOKPROC lpfn,    _In_opt_ HINSTANCE hmod,    _In_ DWORD dwThreadId);
参数介绍:
_In_ int idHook:钩子类型,可取以下值:
WH_MSGFILTER    = -1; 线程级; 截获用户与控件交互的消息
WH_JOURNALRECORD  = 0; 系统级; 记录所有消息队列从消息队列送出的输入消息, 在消息从队列中清除时发生; 可用于宏记录
WH_JOURNALPLAYBACK = 1; 系统级; 回放由 WH_JOURNALRECORD 记录的消息, 也就是将这些消息重新送入消息队列
WH_KEYBOARD    = 2; 系统级或线程级; 截获键盘消息
WH_GETMESSAGE   = 3; 系统级或线程级; 截获从消息队列送出的消息
WH_CALLWNDPROC   = 4; 系统级或线程级; 截获发送到目标窗口的消息, 在 SendMessage 调用时发生
WH_CBT       = 5; 系统级或线程级; 截获系统基本消息, 譬如: 窗口的创建、激活、关闭、最大最小化、移动等等有用的窗体控件消息
WH_SYSMSGFILTER  = 6; 系统级; 截获系统范围内用户与控件交互的消息
WH_MOUSE      = 7; 系统级或线程级; 截获鼠标消息
WH_HARDWARE    = 8; 系统级或线程级; 截获非标准硬件(非鼠标、键盘)的消息
WH_DEBUG      = 9; 系统级或线程级; 在其他钩子调用前调用, 用于调试钩子
WH_SHELL      = 10; 系统级或线程级; 截获发向外壳应用程序的消息
WH_FOREGROUNDIDLE = 11; 系统级或线程级; 在程序前台线程空闲时调用
WH_CALLWNDPROCRET = 12; 系统级或线程级; 截获目标窗口处理完毕的消息, 在 SendMessage 调用后发生
__in HOOKPROC lpfn:回调函数地址
其回调函数原型为:
LRESULT CALLBACK name(int nCode, WPARAM wParam, LPARAM lParam)
name可以随便起,这里来解释一下回调函数的参数:
int nCode:消息代码
 WPARAM wParam:附加参数一
LPARAM lParam:附加参数二
返回值:LRESULT类型(便于传递hook,后面详细介绍)
__in HINSTANCE hMod:DLL实列句柄
__in DWORD dwThreadId:要挂钩的线程ID

函数作用:设置钩子

2.CallNextHookEx


函数原型:


LRESULT

WINAPI

CallNextHookEx(

    _In_opt_ HHOOK hhk,

    _In_ int nCode,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam);

参数介绍:

 _In_opt_ HHOOK hhk:下一个钩子子程的句柄

_In_ int nCode: 钩子消息代码,此消息代码会被传递给下一个钩子处理

 _In_ WPARAM wParam:消息附加参数一

 _In_ LPARAM lParam:消息附加参数二

函数作用:将消息传递给下一个钩子子程序,比如你有多个钩子,当你回调函数处理完之后,可以使用此函数将消息传递给下一个钩子子程处理,也可以选择不传递,在windows里最新钩子子程序每次会第一个获取到此消息,上面说过钩子链表这里就不多说了,同时此函数也可以用来屏蔽消息,后面详细说明!


返回值:LRESULT类型,返回值是hook钩子类型代码


3.UnhookWindowsHookEx


函数原型:

 

BOOL WINAPI UnhookWindowsHookEx( __in HHOOK hhk);

参数介绍:

__in HHOOK hhk:要删除的hook钩子句柄

返回值:成功true否则false


函数作用:释放钩子

下面开始几个列子讲解此函数应该怎样使用:

 


1.截获当前线程键盘消息(局部钩子)


这里我们先新建一个win32程序


#include "stdafx.h"
#include <windows.h>
 
HWND hWnd;//progman
 
//消息函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
//判断消息ID
switch (uMsg){
case WM_DESTROY:    // 窗口销毁消息
PostQuitMessage(0);   //  发送退出消息
return 0;
}
// 其他的消息调用缺省的消息处理程序
return DefWindowProc(hwnd, uMsg, wParam, lParam);
 
}
// 3、注册窗口类型
BOOL RegisterWindow(LPCSTR lpcWndName, HINSTANCE hInstance)
{
ATOM nAtom = 0;
// 构造创建窗口参数
WNDCLASS wndClass = { 0 };
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WindowProc;      // 指向窗口过程函数
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = hInstance;
wndClass.hIcon = NULL;
wndClass.hCursor = NULL;
wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = lpcWndName;    // 注册的窗口名称,并非标题,以后创建窗口根据此注册的名称创建
nAtom = RegisterClass(&wndClass);
return TRUE;
}
//创建窗口(lpClassName 一定是已经注册过的窗口类型)
HWND CreateMyWindow(LPCTSTR lpClassName, HINSTANCE hInstance)
{
HWND hWnd = NULL;
// 创建窗口
hWnd = CreateWindow(lpClassName, "test", WS_OVERLAPPEDWINDOW^WS_THICKFRAME, 0, 0, 1000, 800, NULL, NULL, hInstance, NULL);
return hWnd;
}
//显示窗口
void DisplayMyWnd(HWND hWnd)
{
//获得屏幕尺寸
 
int scrWidth = GetSystemMetrics(SM_CXSCREEN);
int scrHeight = GetSystemMetrics(SM_CYSCREEN);
RECT rect;
GetWindowRect(hWnd, &rect);
ShowWindow(hWnd, SW_SHOW);
//重新设置rect里的值
rect.left = (scrWidth - rect.right) / 2;
rect.top = (scrHeight - rect.bottom) / 2;
//移动窗口到指定的位置
SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW);
UpdateWindow(hWnd);
}
 
void doMessage()        // 消息循环处理函数
{
MSG msg = { 0 };
// 获取消息
while (GetMessage(&msg, NULL, 0, 0)) // 当接收到WM_QIUT消息时,GetMessage函数返回0,结束循环
{
DispatchMessage(&msg); // 派发消息,到WindowPro函数处理
}
}
 
// 入口函数
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd)
{
HWND hWnd = NULL;
LPCTSTR lpClassName = "MyWnd";  // 注册窗口的名称
RegisterWindow(lpClassName, hInstance);
hWnd = CreateMyWindow(lpClassName, hInstance);
DisplayMyWnd(hWnd);
doMessage();
return 0;
}

win32窗口创建代码基本上写完了,我们先定义一个全局变量hook方便回调函数传递hook:


 

//hook

HHOOK hook;

在定义一个回调函数:

//hook回调函数

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)

{

//拦截所有的键盘消息

MessageBox(NULL, "dd", "键盘消息被触发了", 0);

return CallNextHookEx(hook, nCode, wParam, lParam);

}

 

这里来说一下为什么回调函数的类型是LRESULT,通过上面的代码可以发现CallNextHookEx函数返回值是LRESULT类型的,所以想要兼容此函数返回值也必须是LRESULT类型的,其返回值是钩子类型的代码,这个我们无需关系!


注册hook


hook = SetWindowsHookEx(WH_KEYBOARD, LowLevelMouseProc, NULL, GetCurrentThreadId()); //GetCurrentThreadId()API函数可以调用线程的线程ID

 

WH_KEYBOARD只拦截键盘消息


运行效果:


这个时候我们随便按下一个按键:


 成功拦截到此消息了,这里我们在把回调函数里的代码稍微更改一下,只对空格键有效:


 //拦截空格消息

if (wParam == VK_SPACE)

MessageBox(NULL, "空格被按下了, "键盘消息被触发了", 0);

return CallNextHookEx(hook, nCode, wParam, lParam);

//VK_SPACE是空格的键代码,这里我们没有使用键代码到字符消息之间的映射转换,所以我们要直接使用键代码来比对,键盘消息的附加键代码存放在wParam参数里

运行结果:

 

 

无论我们按下什么按键都没有反应,这里我们按下空格试试:


好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***