VCpp钩子使用之全局键盘钩子

⌚Time: 2022-12-14 21:14:39

👨‍💻Author: Jack Ge

如果想要在Win32窗体程序中实现按键捕获,可以覆写PreTranslateMessage函数,但是有一个缺点,就是此方法只有当程序窗口获取到焦点时才可以捕获到键盘,如果在其它程序窗口中按下按键,是捕获不到的,为了解决这个问题,就需要使用到钩子

钩子简介

挂钩是一种机制,应用程序可以通过它截获事件,例如消息、鼠标操作和击键。截获特定类型事件的函数称为挂钩过程。挂钩过程可以对它接收的每个事件执行操作,然后修改或放弃该事件。

钩子分为线程钩子(局部钩子)、系统钩子(全局钩子)

线程钩子监视指定线程的事件消息。

系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL)中。这是系统钩子和线程钩子很大的不同之处。

钩子的类型


WH_CALLWNDPROC和WH_CALLWNDPROCRET

WH_CBT

WH_DEBUG

WH_FOREGROUNDIDLE

WH_GETMESSAGE

WH_JOURNALPLAYBACK

WH_JOURNALRECORD

WH_KEYBOARD_LL

WH_KEYBOARD

WH_MOUSE_LL

WH_MOUSE

WH_MSGFILTER和WH_SYSMSGFILTER

WH_SHELL

如对于WH_KEYBOARD,有对应的回调函数。回调函数形式


LRESULT CALLBACK KeyboardProc(

  _In_ int    code,

  _In_ WPARAM wParam,

  _In_ LPARAM lParam

);

WH_KEYBOARD和WH_KEYBOARD_LL

WH_KEYBOARD_LL是一个Low-Level的钩子, 当Raw Input Thread(RIT)决定从系统消息队列中分发消息之前, 就已经截获了这个消息进行了处理. 所以WH_KEYBOARD_LL甚至会早于系统线程来处理消息, 比如ctrl + alt + del都可以截获, 并且WH_KEYBOARD_LL让系统不需要通过DLL来动态注入所有进程了,系统只会把消息发送到Hook线程. WH_KEYBOARD的层级比较高, 属于应用程序级别的, 所以系统线程会早于这个Hook响应, 并且只能截获系统发送到应用线程messag queue的消息, 但是优先执行该Hook的回调函数,如果Hook回调返回false才会继续处理当前应用的消息响应函数.

这两种钩子都可以作为全局钩子使用, 只需要设置钩子的时候注入线程设成 0

WH_KEYBOARD_LL 在消息发送之前就已经处理了, 而 WH_KEYBOARD是发送到注入线程以后

WH_KEYBOARD_LL 无需系统注入DLL执行, 而 WH_KEYBOARD 需要DLL来注入所有进程空间

使用钩子

使用VS2005新建一个项目TestKeyBoardHook,使用DLL注入的方式使用全局键盘钩子

对于TestKeyBoardHook中的CTestKeyBoardHookDlg类,覆写其中的PreTranslateMessage函数,使窗体接收到按键后能够显示对应的按键




BOOL CTestKeyBoardHookDlg::PreTranslateMessage(MSG* pMsg)

{

    // TODO: 在此添加专用代码和/或调用基类

    //按键按下会多次触发WM_KEYDOWN消息,因此使用WM_KEYUP消息

    if (pMsg->message == WM_KEYUP)

    {

        switch (pMsg->wParam)

        {

        case VK_F5:

            {

                GetDlgItem(IDC_STATIC1)->SetWindowText(L"F5被按下");

            }

            break;

        case VK_F11:

            {

                GetDlgItem(IDC_STATIC1)->SetWindowText(L"F11被按下");

            }

            break;

        case VK_F12:

            {

                GetDlgItem(IDC_STATIC1)->SetWindowText(L"F12被按下");

            }

            break;

        }

    }

    return CDialog::PreTranslateMessage(pMsg);

}


可以看到,当在窗体中按f5等键,能够显示出来,但是窗体失去焦点后,在其它程序中按键,窗体就不能捕获到了

生成KeyHook.dll

对解决方案右键,再添加一个新项目到解决方案,选择MFC DLL

MFC扩展DLL,点击完成

此时共有两个项目,一个KeyHook是要生成的dll,一个TestKeyBoardHook是测试程序

在KeyHook的KeyHook.cpp中定义全局数据段,实现共享数据


#pragma data_seg("myData") //定义全局数据段

HHOOK g_hHook=NULL; //钩子句柄

HINSTANCE g_hInstance=NULL;//dll实例句柄

HWND g_hookWindow=NULL;//窗体句柄

#pragma data_seg()

在KeyHook.def文件中定义段属性


SECTIONS

  myData READ WRITE SHARED

在DllMain函数中


extern "C" int APIENTRY

DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)

{

    // 如果使用 lpReserved,请将此移除

    UNREFERENCED_PARAMETER(lpReserved);



    if (dwReason == DLL_PROCESS_ATTACH)

    {

        TRACE0("KeyHook.DLL 正在初始化!\n");

        

        // 扩展 DLL 一次性初始化

        if (!AfxInitExtensionModule(KeyHookDLL, hInstance))

            return 0;



        // 将此 DLL 插入到资源链中

        // 注意: 如果此扩展 DLL 由

        //  MFC 规则 DLL (如 ActiveX 控件)隐式链接到,

        //  而不是由 MFC 应用程序链接到,则需要

        //  将此行从 DllMain 中移除并将其放置在一个

        //  从此扩展 DLL 导出的单独的函数中。使用此扩展 DLL 的

        //  规则 DLL 然后应显式

        //  调用该函数以初始化此扩展 DLL。否则,

        //  CDynLinkLibrary 对象不会附加到

        //  规则 DLL 的资源链,并将导致严重的

        //  问题。



        new CDynLinkLibrary(KeyHookDLL);//把DLL加入动态MFC类库中

        g_hInstance=hInstance;//插入保存DLL实例句柄



    }

    else if (dwReason == DLL_PROCESS_DETACH)

    {

        TRACE0("KeyHook.DLL 正在终止!\n");

        // 在调用析构函数之前终止该库

        AfxTermExtensionModule(KeyHookDLL);

    }

    return 1;   // 确定

}

添加按键回调函数


LRESULT CALLBACK KeyboardProc(int code// hook code

                              ,WPARAM wParam// virtual-key code

                              ,LPARAM lParam// keystroke-message information

                              )

{

    if (code >= 0)

    {

        //如果正在抬起按键,lParam的第31位置1,正在按下第31位置0,这里只检测按键抬起后发送消息给目标窗体

        if(lParam&0x80000000){

            PostMessage(g_hookWindow,WM_KEYUP,wParam,lParam);

        }

    }

    return CallNextHookEx(g_hHook, code, wParam, lParam);

}

添加安装和卸载dll的函数,为了使函数符号命名按照C语言的形式,使用extern "C"修饰


extern "C" void install_dll(HWND hookWindow){

    g_hHook=SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,g_hInstance,0);

    g_hookWindow = hookWindow;

}

extern "C" void uninstall_dll()

{

    UnhookWindowsHookEx(g_hHook);

    g_hHook = NULL;

}

在KeyHook.def文件中导出函数install_dll和uninstall_dll


EXPORTS

    ; 此处可以是显式导出

    install_dll @1

    uninstall_dll @2

之后右键KeyHook项目,生成,得到了KeyHook.dll和KeyHook.lib文件

加载KeyHook.dll

对于动态库的加载,有静态加载和动态加载两种方式,对于这里使用动态加载的方式载入

在CTestKeyBoardHookDlg.cpp文件中定义函数指针类型


typedef void (*InstallDll)(HWND);

typedef void (*UnInstallDll)(void);

定义全局变量,函数指针和dll句柄


InstallDll installDll;

UnInstallDll unInstallDll;

HINSTANCE hDll;

在CTestKeyBoardHookDlg::OnInitDialog函数中动态载入dll,安装钩子




BOOL CTestKeyBoardHookDlg::OnInitDialog()

{

    CDialog::OnInitDialog();



    // 将“关于...”菜单项添加到系统菜单中。



    // IDM_ABOUTBOX 必须在系统命令范围内。

    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);

    ASSERT(IDM_ABOUTBOX < 0xF000);



    CMenu* pSysMenu = GetSystemMenu(FALSE);

    if (pSysMenu != NULL)

    {

        CString strAboutMenu;

        strAboutMenu.LoadString(IDS_ABOUTBOX);

        if (!strAboutMenu.IsEmpty())

        {

            pSysMenu->AppendMenu(MF_SEPARATOR);

            pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);

        }

    }



    // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动

    //  执行此操作

    SetIcon(m_hIcon, TRUE);         // 设置大图标

    SetIcon(m_hIcon, FALSE);        // 设置小图标



    // TODO: 在此添加额外的初始化代码



    //动态加载dll

    hDll=LoadLibrary(L"KeyHook.dll");

    installDll = (InstallDll)GetProcAddress(hDll,"install_dll");

    unInstallDll = (UnInstallDll)GetProcAddress(hDll,"uninstall_dll");

    //安装钩子

    installDll(GetSafeHwnd());

    return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE

}

在析构函数中卸载钩子


CTestKeyBoardHookDlg::~CTestKeyBoardHookDlg(){

    unInstallDll();

    FreeLibrary(hDll);

}

之后再次编译运行TestKeyBoardHook,发现在窗口失去焦点,其它程序中按键,程序也可以捕获到按键并显示

注意事项:

编译的dll和测试程序都对应debug版本或者release版本,都是win32位

窗体接收两次按键消息的问题

由于在钩子回调函数中使用PostMessage向窗体发送了一次按键消息,而之后按键消息会再次被传递到窗体,因此窗体接收了两次按键消息,解决办法是在回调函数中向窗体发送消息后直接返回ture,这样就会拦截该消息,不再向下传递


        if(lParam&0x80000000){

            PostMessage(g_hookWindow,WM_KEYUP,wParam,lParam);

            return true;

        }

参考

MSDN

https://learn.microsoft.com/en-us/windows/win32/winmsg/about-hooks#wh_keyboard