跳至主要內容

62-API Hooking - 如何使用 Windows API

Maldevacademy大约 5 分钟安全开发

简介

SetWindowsHookExopen in new window WinAPI 调用是一种替代 API 钩子方法。它主要用于跟踪特定类型的系统事件,这与早期模块中使用的技术不同,因为 SetWindowsHookExW/A 不会修改函数的功能,而是会在触发某个事件时执行一个回调函数。此类事件的类型受限于 Windows 提供的类型。

SetWindowsHookEx 使用方法

SetWindowsHookExW WinAPI 如下所示。

HHOOK SetWindowsHookExW(
  [in] int       idHook,      // 要安装的钩子过程类型
  [in] HOOKPROC  lpfn,        // 指向钩子过程(要执行的函数)的指针
  [in] HINSTANCE hmod,        // 包含钩子过程的 DLL 句柄(保留为 NULL)
  [in] DWORD     dwThreadId   // 与要关联钩子过程的线程 ID(保留为 NULL)
);
  • idHook - 要监视的事件。例如,WH_KEYBOARD_LL 标志用于监视击键消息,可以充当按键记录器。请注意,使用 SetWindowsHookEx 执行按键记录是一个老技巧。对于此模块,将使用 WH_MOUSE_LL 标志来监视鼠标单击。

  • lpfn - 指向在指定事件发生时执行的回调函数的指针。在本例中,该函数将在每次鼠标单击时执行。

回调函数

回调函数应为 HOOKPROC 类型,如下所示:

typedef LRESULT (CALLBACK* HOOKPROC)(int nCode, WPARAM wParam, LPARAM lParam);

因此,应将回调函数定义为如下所示:

LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
  // 函数代码
}

回调函数还应使用 CallNextHookExopen in new window WinAPI 并返回其输出。CallNextHookEx 将挂钩信息传递给挂钩链中的下一个挂钩过程。换句话说,它将在下次执行时将挂钩的信息传递给回调函数。

已更新回调函数以包含 CallNextHookEx

LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
  // 函数代码

  return CallNextHookEx(NULL, nCode, wParam, lParam)
}

根据 Microsoft 的 备注部分open in new window,调用 CallNextHookEx 是可选的,但强烈建议这样做。否则,已安装挂钩的其他应用程序将不会接收挂钩通知,并且可能行为不正确。

最后,最后一部分是回调函数的代码。该代码将监视操作,因此在此示例中,函数通过以下代码检查单击了哪个鼠标按钮。

LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
    
    if (wParam == WM_LBUTTONDOWN){
        printf("[ # ] 鼠标左键点击 \n");
    }
    
    if (wParam == WM_RBUTTONDOWN) {
        printf("[ # ] 鼠标右键点击 \n");
    }
    
    if (wParam == WM_MBUTTONDOWN) {
        printf("[ # ] 鼠标中键点击 \n");
    }

  return CallNextHookEx(NULL, nCode, wParam, lParam)
}

处理消息

获取完用于监视用户鼠标点击的代码后,下一步是确保挂钩进程得以维持。这是通过在特定时间段内执行监视代码来实现的。为此,在使用 WaitForSingleObject WinAPI 让线程保持活动状态所需的持续时间内,在该线程中调用 SetWindowsHookExW

// 只要用户点击鼠标按钮,就会执行该回调函数
LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam) {

    if (wParam == WM_LBUTTONDOWN) {
        printf("[ # ] 左键点击 \n");
    }

    if (wParam == WM_RBUTTONDOWN) {
        printf("[ # ] 右键点击 \n");
    }

    if (wParam == WM_MBUTTONDOWN) {
        printf("[ # ] 中键点击 \n");
    }

    // 移动到挂钩链中的下一个挂钩
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}


BOOL MouseClicksLogger() {

    // 安装钩子
    HHOOK hMouseHook = SetWindowsHookExW(
        WH_MOUSE_LL,           // 鼠标全局挂钩类型
        (HOOKPROC)HookCallback, // 回调函数
        NULL,                // 实例句柄,此处为 NULL
        NULL                  // 不支持线程(多重)挂钩,因此值为 NULL
    );

    if (!hMouseHook) {
        printf("[!] SetWindowsHookExW with Error : %d \n", GetLastError());
        return FALSE;
    }

    // 保持线程运行
    while (1) {

    }

    return TRUE;
}


int main() {

    HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);

    if (hThread)
        WaitForSingleObject(hThread, 10000); // 监视鼠标点击 10 秒

    return 0;
}

改善实现

之前代码的问题在于 while 循环未能处理挂钩的鼠标消息,导致目标计算机上的鼠标移动出现延迟。要解决此问题,需要使用 [DefWindowProc](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw) 处理所有消息事件。这将确保事件得到系统正确处理,并执行任何关联的默认行为。DefWindowProcW 调用默认窗口过程为任何未由应用程序处理的窗口消息提供默认处理。

若要获取消息的详细信息,必须首先调用 [GetMessageW](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew),它从调用线程的消息队列中检索消息。然后将此消息传递给 DefWindowProcW 进行处理。GetMessageW[MSG 结构](https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg) 中返回消息信息,其中包括后续 DefWindowProcW 调用所需的所有内容。

所有这些都应在循环中执行,以确保手动处理每个未处理的消息。

// 每当用户单击鼠标按钮时执行的回调函数
LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam){

    if (wParam == WM_LBUTTONDOWN){
        printf("[ # ] 左键单击 \n");
    }
    
    if (wParam == WM_RBUTTONDOWN) {
        printf("[ # ] 右键单击 \n");
    }
    
    if (wParam == WM_MBUTTONDOWN) {
        printf("[ # ] 中键单击 \n");
    }
    
    // 移动到挂钩链中的下一个挂钩
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}


BOOL MouseClicksLogger(){
    
    MSG         Msg         = { 0 };

    // 安装挂钩
    HHOOK hMouseHook = SetWindowsHookExW(
        WH_MOUSE_LL,
        (HOOKPROC)HookCallback,
        NULL,   
        NULL
    );
    if (!hMouseHook) {
        printf("[!] SetWindowsHookExW 失败,错误代码:%d \n", GetLastError());
        return FALSE;
    }

    // 处理未处理的事件
    while (GetMessageW(&Msg, NULL, NULL, NULL)) {
        DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
    }
    
    return TRUE;
}


int main() {
  
    HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);
    if (hThread)
        WaitForSingleObject(hThread, 10000); // 监测鼠标单击 10 秒

    return 0;
}

移除钩子

要移除用 SetWindowsHookEx 函数安装的任何钩子,都必须调用 WinAPI 函数 UnhookWindowsHookExopen in new windowUnhookWindowsHookEx 只接受对要移除的钩子的句柄。

SetWindowsHookEx 钩子代码

下面的代码段将本模块中讨论的所有内容付诸实践,对鼠标点击事件进行钩取,然后移除钩子。

// 全局钩子句柄变量
HHOOK g_hMouseHook = NULL;


// 每当用户单击鼠标按钮时执行的回调函数
LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam){

    if (wParam == WM_LBUTTONDOWN){
        printf("[ # ] 鼠标左键点击 \n");
    }
    
    if (wParam == WM_RBUTTONDOWN) {
        printf("[ # ] 鼠标右键点击 \n");
    }
    
    if (wParam == WM_MBUTTONDOWN) {
        printf("[ # ] 鼠标中键点击 \n");
    }
    
    // 移动到钩子链中的下一个钩子
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}


BOOL MouseClicksLogger(){
    
    MSG         Msg         = { 0 };

    // 安装钩子
    g_hMouseHook = SetWindowsHookExW(
        WH_MOUSE_LL,
        (HOOKPROC)HookCallback,
        NULL,   
        NULL
    );
    if (!g_hMouseHook) {
        printf("[!] SetWindowsHookExW 发生错误:%d \n", GetLastError());
        return FALSE;
    }

    // 处理未处理的事件
    while (GetMessageW(&Msg, NULL, NULL, NULL)) {
        DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
    }
    
    return TRUE;
}


int main() {
  
    HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);
    if (hThread)
        WaitForSingleObject(hThread, 10000); // 监控鼠标点击 10 秒

    // 卸载钩子
    if (g_hMouseHook && !UnhookWindowsHookEx(g_hMouseHook)) {
        printf("[!] UnhookWindowsHookEx 发生错误:%d \n", GetLastError());
    }
    return 0;
}

示例

[图片]