0%

《逆向工程核心原理》学习笔记12

逆向工程核心原理第二十一章

Windows消息钩取

钩子(Hook)
  • 英文Hook一词,翻成中文是"钩子"、"鱼钩"的意思,泛指勾取所需东西而使用的一切工具。"钩子"这一基本含义被延伸发展为"偷看或截取信息时所用的手段或工具"

    image-20211208144510124

消息钩子
  • Windows操作系统向用户提供GUI(Graphic User Interface,图形用户界面),它事件驱动(Event Driven)方式工作。在操作系统中借助键盘、鼠标,选择菜单、按钮,以及移动鼠标、改遍窗口大小与位置等都是事件(Event)。发生这样的事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应的动作。也就是说,敲击键盘时,消息会从OS移动到应用程序。所谓的"消息钩子"就在此间偷看这些信息。

  • 常规Windows消息流

    • 发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]
    • OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序的[application message queue]中
    • 应用程序(如记事本)监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理
  • 如下图所示,OS消息队列与应用程序消息队列之间存在一条"钩链"(Hook Chain),设置好键盘消息钩子之后,处于"钩链"中的键盘消息钩子会比应用程序先看到相应信息。在键盘消息钩子函数内部,除了可以查看消息之外,还可以修改消息本身,而且还能对消息实施拦截,阻止消息传递。

    image-20211208144510124

  • 像这样的消息钩子功能是Windows操作系统提供的基本功能,其中最具代表性的是MS Visual Studio中提供的SPY++,它是一个功能十分强大的消息钩起程序,能够查看操作系统中来往的所有消息。

SetWindowsHookEx()
  • 使用SetWindowsHookEx() API可轻松实现消息钩子,SetWindowsHookEx() API的定义如下

    1
    2
    3
    4
    5
    6
    HHOOK SetWindowsHookEx(
    int idHook; // hook type
    HOOKPROC lpfn; // hook procedure
    HINSTANCE hMod; // hook procedure所属的DLL句柄
    DWORD dwThreadID // 想要挂钩的线程ID
    );
  • 钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息"钩子"时,"钩子"过程需要存在于某个DLL内部,且该DLL的实例句柄(instance handle)即是hMod。

    若dwThreadID参数被设置为0,则安装的钩子为"全局钩子"(Global Hook),它会影响到运行中的(以及以后要运行的)所有进程

键盘消息钩取练习
  • 下面做一个简单的键盘消息钩取练习,以进一步加深对前面内容的理解

    image-20211210095813040

  • KeyHook.dll文件是一个含有钩子的过程(KeyboardProc)的DLL文件。HooMain.exe是最先加载KeyHook.dll并安装键盘钩子的程序。HookMain.exe加载KeyHook.dll文件后使用SetWindowsHookEx()安装键盘钩子(KeyboardProc)。若其他进程(explore.exe、iexplore.exe、notepad.exe等)中发生键盘输入事件,OS就会强制将KeyHook.dll加载到相应进程的内存,然后调用KeyboardProc()函数。

练习示例HooKMain.exe
  • 从《逆向工程核心原理》书的官方githubhttps://github.com/reversecore/book.git,下载HookMain的源代码(有可执行程序,但是64位系统的不能使用,64位系统使用32位的Hook程序会直接卡死,所以需要自己编译测试)

    一开始直接用32位的编译程序在64位的win10上运行,发现一直卡死。搜索后,在这篇文章找到了原因windows10 记事本进程 键盘消息钩子 dll注入,原因是:SetWindowsHookEx的官方文档提到了这个API只能用于32位程序注入32位程序或者64位注入64位程序,否则会直接卡死。

  • 下载源代码后,用visual studio打开HookMain和KeyHook项目后,新建一个x64平台的活动解决方案,然后生成即可

    image-20211210111753611

  • 编译生成好HookMain.exe和KeyHook.dll后,将两者放在一个文件夹下,运行HookMain.exe,安装键盘钩子

    image-20211210112040141

  • 接着运行notepad.exe,使用Process Explore查看notepad.exe进程,在Process Explore选项卡中选择View DLLs(Ctrl + D)即可查看进程加载了哪些DLL文件,如下图所示,可以看到KeyHook.dll已经注入到notepad.exe进程中

    image-20211210112350891

  • 在Process Explorer中检索(Find Handle or DLL ... Ctrl + Shift + F)注入KeyHook.dll的所有进程,如下图所示,一个进程开始运行并发生键盘事件时,KeyHook.dll就会注入其中(但忽视键盘事件的仅有notepad.exe进程,其他进程会正常处理键盘事件)

    image-20211210112903242

  • 按"q",拆除键盘钩子,在notepad.exe中使用键盘输入,可以发现又能正常输入了。在Process Explorer中检索KeyHook会发现,没有进程加载KeyHook.dll

    image-20211210113257063

    image-20211210113541186

  • 拆除键盘钩子后,相关进程就会将KeyHook.dll文件全部卸载

分析源代码
  • 下载书中的源代码,首先看一下HookMain.cpp的源代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    #include "stdio.h"
    #include "conio.h"
    #include "windows.h"

    #define DEF_DLL_NAME "KeyHook.dll"
    #define DEF_HOOKSTART "HookStart"
    #define DEF_HOOKSTOP "HookStop"

    typedef void (*PFN_HOOKSTART)();
    typedef void (*PFN_HOOKSTOP)();

    void main()
    {
    HMODULE hDll = NULL;
    PFN_HOOKSTART HookStart = NULL;
    PFN_HOOKSTOP HookStop = NULL;
    char ch = 0;

    // 加载 KeyHook.dll
    hDll = LoadLibraryA(DEF_DLL_NAME);
    if( hDll == NULL )
    {
    printf("LoadLibrary(%s) failed!!! [%d]", DEF_DLL_NAME, GetLastError());
    return;
    }

    // 获取导出函数地址
    HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
    HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

    // 启动Hook
    HookStart();

    // 等待用户输入"q"
    printf("press 'q' to quit!\n");
    while( _getch() != 'q' );

    // 停止Hook
    HookStop();

    // 卸载KeyHook.dll
    FreeLibrary(hDll);
    }
  • HookMain.exe的源代码非常简单,先加载KeyHook.dll文件,然后调用HookStart()函数开始钩取,用户输入"q"时,调用HookStop()函数终止钩取。

  • 接下来看看KeyHook.dll文件的源代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    #include "stdio.h"
    #include "windows.h"

    #define DEF_PROCESS_NAME "notepad.exe"

    HINSTANCE g_hInstance = NULL;
    HHOOK g_hHook = NULL;
    HWND g_hWnd = NULL;

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
    {
    switch( dwReason )
    {
    case DLL_PROCESS_ATTACH:
    g_hInstance = hinstDLL;
    break;

    case DLL_PROCESS_DETACH:
    break;
    }

    return TRUE;
    }

    LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
    {
    char szPath[MAX_PATH] = {0,};
    char *p = NULL;

    if( nCode >= 0 )
    {
    // bit 31 : 0 => press, 1 => release
    if( !(lParam & 0x80000000) ) // 释放键盘按键时
    {
    GetModuleFileNameA(NULL, szPath, MAX_PATH);
    p = strrchr(szPath, '\\');
    // 比较当前进程名称,若为notepad.exe则消息不会传递给应用程序(或下一个"钩子")
    if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
    return 1;
    }
    }

    // 若非notepad.exe,则调用CallNextHooxEx()函数,将消息传递给应用程序(或下一个"钩子")
    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
    }

    #ifdef __cplusplus
    extern "C" {
    #endif
    __declspec(dllexport) void HookStart()
    {
    g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
    }

    __declspec(dllexport) void HookStop()
    {
    if( g_hHook )
    {
    UnhookWindowsHookEx(g_hHook);
    g_hHook = NULL;
    }
    }
    #ifdef __cplusplus
    }
    #endif
  • DLL代码部分,调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链

  • MSDN中对KeyboardProc函数的定义如下:

    1
    2
    3
    4
    5
    LRESULT CALLBACK KeyboardProc(
    int code, // HC_ACTION(0), HC_NOREMOVE(3)
    WPARAM wParam, // virtual-key code
    LPARAM lParam // extra information
    )
  • 安装好键盘"钩子"后,无论哪个进程,只要发生键盘输入事件,OS就会强制将KeyHook.dll注入相应进程。加载了KeyHook.dll的进程中,发生键盘事件时,就会首先调用执行KeyHook.KeyboardProc()

  • KeyboardProc()函数作用,判断当前发生键盘事件的进程是否为notepad.exe,如果是notepad.exe,那么返回1(拦截键盘事件,不往下传递),否则将键盘事件原封不动地传递给应用程序(或者下一个钩链)

调试练习
  • 因为上面编译的HookMain.exe和KeyHook.dll都是64位的,Ollydbg不支持64可执行文件,所以这里使用x64dbg进行调试

  • 因为HookMain.exe有字符串"press 'q' to quit!",字符串,所以这里可以使用搜索字符串定位到核心代码,如下图所示,图中代码就是HookMain.exe到main()函数

    image-20211210164854939

  • 可以看到64位程序的寄存器名称都变成了R开头,但跟32位的寄存器大同小异。在"140069910"地址处下断点,然后运行程序,到断点处停下来,开始调试。从断点处开始依次跟踪调试代码,可以了解main()中的主要代码流。

  • 先在"14006994E"地址处调用LoadLibrary("KeyHook.dll"),然后在"1400699A7"地址处调用HookStart()函数,该处执行后notepad.exe的键盘事件就会被拦截

    image-20211210171134527

  • F7步入查看HookStart()函数内容,可以看到在"1800698F1"调用了SetWindowsHookExW()函数

    image-20211210171339846

调试notepad.exe进程内的KeyHook.dll
  • 先启动HookMain.exe

  • 用x64dbg调试notepad.exe,F9让notepad.exe正常运行,然后在x64dbg的选项中勾选DLL入口,这样当新的DLL装入时,调试器会在DLL入口处停止(调试完后再取消勾选)。

    image-20211210172730064

  • 在notepad.exe界面中,键盘输入触发KeyHook.dll装载。x64dbg中选择内存布局选项卡,可以看到keyhook.dll被装载在180000000地址处

    image-20211210173108633

  • 点击CPU选项卡,回到主界面,可以看到,此时调试器停在KeyHook.dll的EP,

    image-20211210173314132

  • 如下图所示,F7跟进"18006A41C"处的调用dllmain_dispatch,从函数名来看,这个函数就是DLLMain函数的转发的函数

    image-20211210173443709

  • 继续调试,可以找到DLLMain函数调用的地方,如下图所示,在"18006A243"处调用了KeyHook.dll的DLLMain函数

    image-20211210192921720

  • F7步入"18006A243"该处的调用,就可以看到DLLMain函数,如下图所示

    image-20211210193300985

  • KeyHook.dll的KeyboardProc函数则可以通过搜索字符串"notepad.exe"来定位