首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# Windows Hook 鉤子機制完整指南:從原理到全域鍵盤/滑鼠監控實作

     編輯:關於C語言

Windows Hook(鉤子)是一種強大的訊息攔截機制,允許應用程式在訊息到達目標視窗前進行監控、修改或阻擋。本文系統整理 Hook 核心概念、14 種 Hook 類型、鏈結運作原理、安裝/卸載 API,並提供一個可直接用於 C# 專案的**全域鍵盤/滑鼠監控類別完整原始碼**。

核心概念速覽

訊息機制與 Hook 切入點

Windows 以訊息驅動:使用者輸入(鍵盤/滑鼠)、系統事件、應用程式發送訊息,皆會進入系統訊息佇列,再分派至目標視窗程序。Hook 允許你在訊息分派**之前**插入自訂回呼函數,形成一條 **Hook Chain(鉤子鏈)**。

Hook 範圍

  • 區域 Hook (Local/Thread-specific): 僅監控當前執行緒/進程的訊息。回呼函數可直接寫在 EXE 中。
  • 全域 Hook (Global/System-wide): 監控**所有進程**的特定訊息。回呼函數**必須置於 DLL 中**,由系統注入各進程位址空間。
  • 低階 Hook (Low-level, `LL` 尾綴): `WH_KEYBOARD_LL`、`WH_MOUSE_LL`。雖屬全域,但**不需 DLL**,回呼在安裝進程內執行(效能較差,適合簡單監控)。

14 種常用 Hook 類型對照表

常數值用途備註
WH_KEYBOARD2鍵盤訊息 (WM_KEYDOWN/UP)需 DLL 全域
WH_MOUSE7滑鼠訊息需 DLL 全域
WH_KEYBOARD_LL13低階鍵盤攔截**免 DLL**,全域
WH_MOUSE_LL14低階滑鼠攔截**免 DLL**,全域
WH_GETMESSAGE3GetMessage/PeekMessage 返回前可修改訊息
WH_CALLWNDPROC4SendMessage 呼叫前不可修改訊息
WH_CBT5視窗建立/啟動/銷毀等可阻擋視窗操作
WH_SHELL10Shell 事件(視窗建立/工作列)需註冊才收到
WH_JOURNALRECORD0記錄輸入事件全域,免 DLL
WH_JOURNALPLAYBACK1回放輸入事件全域,免 DLL

核心 API 簽名

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

[DllImport("user32.dll", SetLastError = true)]
static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
  • idHook:Hook 類型常數。
  • lpfn:回呼委派,**務必保持委派實例存活**(防止 GC 回收導致 Crash)。
  • hMod:包含回呼函數的模組控制代碼。全域 Hook (非 LL) 傳 DLL Handle;區域 Hook 或 LL Hook 傳 Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]) 或 IntPtr.Zero。
  • dwThreadId:0 = 全域;特定執行緒 ID = 區域/遠端執行緒。

回呼函數關鍵邏輯

IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0) // HC_ACTION = 0,表示處理訊息
    {
        // 解析 lParam 結構體 (如 KBDLLHOOKSTRUCT, MSLLHOOKSTRUCT)
        // 觸發 C# 事件
        // 若設定 handled = true,可回傳 1 阻斷訊息傳遞
    }
    return CallNextHookEx(_hookHandle, nCode, wParam, lParam); // 傳遞給下一個 Hook
}

完整可用類別:UserActivityHook (全域鍵盤/滑鼠監控)

以下類別封裝了 `WH_KEYBOARD_LL` 與 `WH_MOUSE_LL`,提供 OnMouseActivity、KeyDown、KeyPress、KeyUp 事件,開箱即用:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace GlobalHooks
{
    public class UserActivityHook : IDisposable
    {
        #region P/Invoke
        [StructLayout(LayoutKind.Sequential)]
        private struct POINT { public int x, y; }

        [StructLayout(LayoutKind.Sequential)]
        private struct MouseLLHookStruct { public POINT pt; public int mouseData, flags, time, dwExtraInfo; }

        [StructLayout(LayoutKind.Sequential)]
        private struct KeyboardHookStruct { public int vkCode, scanCode, flags, time, dwExtraInfo; }

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll")]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern short GetKeyState(int vKey);

        [DllImport("user32.dll")]
        private static extern int ToAscii(int uVirtKey, int uScanCode, byte[] lpbKeyState, byte[] lpwTransKey, int fuState);

        [DllImport("user32.dll")]
        private static extern int GetKeyboardState(byte[] pbKeyState);

        private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
        #endregion

        #region Constants
        private const int WH_MOUSE_LL = 14;
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_MOUSEMOVE = 0x200;
        private const int WM_LBUTTONDOWN = 0x201;
        private const int WM_RBUTTONDOWN = 0x204;
        private const int WM_MBUTTONDOWN = 0x207;
        private const int WM_LBUTTONUP = 0x202;
        private const int WM_RBUTTONUP = 0x205;
        private const int WM_MBUTTONUP = 0x208;
        private const int WM_MOUSEWHEEL = 0x020A;
        private const int WM_KEYDOWN = 0x100;
        private const int WM_KEYUP = 0x101;
        private const int WM_SYSKEYDOWN = 0x104;
        private const int WM_SYSKEYUP = 0x105;
        private const byte VK_SHIFT = 0x10;
        private const byte VK_CAPITAL = 0x14;
        private const byte VK_NUMLOCK = 0x90;
        #endregion

        #region Fields & Events
        private IntPtr _mouseHook = IntPtr.Zero;
        private IntPtr _keyboardHook = IntPtr.Zero;
        private HookProc _mouseProc;
        private HookProc _keyboardProc;

        public event MouseEventHandler OnMouseActivity;
        public event KeyEventHandler KeyDown;
        public event KeyPressEventHandler KeyPress;
        public event KeyEventHandler KeyUp;
        #endregion

        public UserActivityHook(bool installMouse = true, bool installKeyboard = true)
        {
            Start(installMouse, installKeyboard);
        }

        public void Start(bool installMouse = true, bool installKeyboard = true)
        {
            if (installMouse && _mouseHook == IntPtr.Zero)
            {
                _mouseProc = MouseHookProc;
                _mouseHook = SetWindowsHookEx(WH_MOUSE_LL, _mouseProc, 
                    Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
                if (_mouseHook == IntPtr.Zero) ThrowWin32();
            }
            if (installKeyboard && _keyboardHook == IntPtr.Zero)
            {
                _keyboardProc = KeyboardHookProc;
                _keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, _keyboardProc,
                    Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
                if (_keyboardHook == IntPtr.Zero) ThrowWin32();
            }
        }

        public void Stop()
        {
            if (_mouseHook != IntPtr.Zero) { UnhookWindowsHookEx(_mouseHook); _mouseHook = IntPtr.Zero; }
            if (_keyboardHook != IntPtr.Zero) { UnhookWindowsHookEx(_keyboardHook); _keyboardHook = IntPtr.Zero; }
        }

        public void Dispose() => Stop();

        ~UserActivityHook() => Stop();

        private void ThrowWin32() => throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());

        #region Hook Procs
        private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && OnMouseActivity != null)
            {
                var info = Marshal.PtrToStructure(lParam);
                MouseButtons btn = MouseButtons.None;
                int clicks = 0, delta = 0;

                switch ((int)wParam)
                {
                    case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_LBUTTONDBLCLK: btn = MouseButtons.Left; break;
                    case WM_RBUTTONDOWN: case WM_RBUTTONUP: case WM_RBUTTONDBLCLK: btn = MouseButtons.Right; break;
                    case WM_MBUTTONDOWN: case WM_MBUTTONUP: btn = MouseButtons.Middle; break;
                    case WM_MOUSEWHEEL: delta = (short)((info.mouseData >> 16) & 0xFFFF); break;
                }
                if (btn != MouseButtons.None)
                    clicks = (wParam == (IntPtr)WM_LBUTTONDBLCLK || wParam == (IntPtr)WM_RBUTTONDBLCLK) ? 2 : 1;

                var e = new MouseEventArgs(btn, clicks, info.pt.x, info.pt.y, delta);
                OnMouseActivity(this, e);
            }
            return CallNextHookEx(_mouseHook, nCode, wParam, lParam);
        }

        private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            bool handled = false;
            if (nCode >= 0 && (KeyDown != null || KeyUp != null || KeyPress != null))
            {
                var info = Marshal.PtrToStructure(lParam);
                int vk = info.vkCode;
                Keys key = (Keys)vk;

                if ((KeyDown != null) && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
                {
                    var e = new KeyEventArgs(key);
                    KeyDown(this, e);
                    handled |= e.Handled;
                }

                if (KeyPress != null && wParam == (IntPtr)WM_KEYDOWN)
                {
                    byte[] ks = new byte[256]; GetKeyboardState(ks);
                    byte[] buf = new byte[2];
                    if (ToAscii(vk, info.scanCode, ks, buf, info.flags) == 1)
                    {
                        char ch = (char)buf[0];
                        bool shift = (GetKeyState(VK_SHIFT) & 0x80) != 0;
                        bool caps = (GetKeyState(VK_CAPITAL) & 1) != 0;
                        if ((shift ^ caps) && char.IsLetter(ch)) ch = char.ToUpper(ch);
                        var e = new KeyPressEventArgs(ch);
                        KeyPress(this, e);
                        handled |= e.Handled;
                    }
                }

                if (KeyUp != null && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
                {
                    var e = new KeyEventArgs(key);
                    KeyUp(this, e);
                    handled |= e.Handled;
                }
            }
            return handled ? (IntPtr)1 : CallNextHookEx(_keyboardHook, nCode, wParam, lParam);
        }
        #endregion
    }
}

使用範例

public partial class MainForm : Form
{
    private UserActivityHook _hook;

    public MainForm()
    {
        InitializeComponent();
        _hook = new UserActivityHook(true, true);
        _hook.OnMouseActivity += (s, e) => lblMouse.Text = $"{e.Button} @ ({e.X}, {e.Y})";
        _hook.KeyDown += (s, e) => lblKey.Text = $"Down: {e.KeyCode}";
        _hook.KeyPress += (s, e) => lblKeyPress.Text = $"Press: {e.KeyChar}";
        _hook.KeyUp += (s, e) => lblKeyUp.Text = $"Up: {e.KeyCode}";
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _hook?.Dispose();
        base.OnFormClosing(e);
    }
}

效能與安全性警示

  • 全域 Hook 會影響系統效能:每個鍵盤/滑鼠事件都會觸發 managed/unmanaged 邊界切換。僅在必要時啟用,用畢即釋放。
  • 低階 Hook (LL) 有超時機制:若回呼執行超過 **~300ms**,系統會自動移除 Hook。回呼內**嚴禁耗時操作**(IO、DB、MessageBox),僅做快速解析並發事件。
  • UAC / UIPI 限制:低權限進程無法 Hook 高權限 (Admin) 進程。若需監控 Admin 視窗,本程式需以 Administrator 執行。
  • 防毒軟體誤報:全域鍵盤 Hook 常被視為鍵盤記錄器特徵,請簽署程式碼或加入白名單。

結語

Windows Hook 是雙面刃:功能強大但責任重大。掌握「區域 vs 全域」、「一般 vs 低階」、「DLL 注入 vs 免 DLL」的選型邏輯,並嚴守「回呼極速返回、委派防 GC、用畢即卸載」三大鐵律,即能在 C# 安全驅馭這項底層技術,實作熱鍵、巨集錄製、使用者行為分析等進階功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved