キーロガーのようなものを作る必要が出てきたので、グローバルフック / キーボードフックについて調べました。以下にざっとその内容をまとめます。
ここでいう「キーロガー」は、打たれたキーをロギングする仕組みという純粋に技術的な意味です。本記事では、その基盤となる Windows のフック機構について整理します。
グローバルフックとは?
フック(Hook)の基本概念
フックとは、
Windows がイベントを処理する途中に、自分の処理を割り込ませる仕組み
です。
通常、キー入力は次のように処理されます。
キーボード
↓
Windows メッセージキュー
↓
対象アプリ
フックを使うと、この途中段階で処理を受け取ることができます。
グローバルフックとは?
フックには大きく2種類あります。
| 種類 | 対象 |
|---|---|
| スレッドフック | 特定スレッドのみ |
| グローバルフック | システム全体 |
グローバルフックは、
すべてのプロセスに届くイベントを監視できる仕組み
です。
グローバルフックでできること
- システム全体のキー入力監視
- マウス操作監視
- 独自ショートカット実装
- 入力分析やログ収集
「どのアプリがアクティブか」に関係なく、入力を取得できるのが特徴です。
グローバルフックのAPI:SetWindowsHookEx
Windowsでは以下のAPIを使います。
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
引数の意味
| 引数 | 内容 |
|---|---|
| idHook | フックの種類 |
| lpfn | コールバック関数 |
| hMod | モジュールハンドル |
| dwThreadId | 対象スレッドID(0でグローバル) |
キーボードフックで使う定数
WH_KEYBOARD_LL = 13
これは Low-Level Keyboard Hook を意味します。
特徴:
- システム全体を対象
- DLLインジェクション不要
- 入力メッセージ生成前に取得可能
キーボードフックとは?
キーボードフックとは、
キーが押された瞬間 / 離された瞬間を取得する仕組み
です。
今回のサンプルでは、
WH_KEYBOARD_LL
を使用しています。
SetWindowsHookEx の呼び出し
SetWindowsHookEx(WH_KEYBOARD_LL, proc, hModule, 0);
ポイント:
WH_KEYBOARD_LLdwThreadId = 0→ グローバルhModule→GetModuleHandleで取得
サンプルクラス
低レベルキーボードフックを .NET で扱えるようにラップしたクラス。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Sample;
public sealed class KeyboardHook : IDisposable
{
// Low-level keyboard hook
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private readonly HookProc _proc;
private nint _hookId;
public event EventHandler<KeyPressedEventArgs>? KeyPressed;
public KeyboardHook()
{
_proc = HookCallback;
}
public void Start()
{
if (_hookId != nint.Zero) return;
_hookId = SetHook(_proc);
if (_hookId == nint.Zero)
{
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error(), "Failed to set keyboard hook.");
}
}
public void Stop()
{
if (_hookId == nint.Zero) return;
UnhookWindowsHookEx(_hookId);
_hookId = nint.Zero;
}
public void Dispose()
{
Stop();
GC.SuppressFinalize(this);
}
private static nint SetHook(HookProc proc)
{
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule!;
var hModule = GetModuleHandle(curModule.ModuleName);
// For WH_KEYBOARD_LL, hMod is ok; threadId must be 0 for global
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, hModule, 0);
}
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
if (nCode >= 0)
{
int msg = unchecked((int)wParam);
if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN)
{
var data = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
// vkCode is what we want (virtual-key code)
KeyPressed?.Invoke(this, new KeyPressedEventArgs((int)data.vkCode, data.flags));
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
public sealed class KeyPressedEventArgs : EventArgs
{
public KeyPressedEventArgs(int vkCode, uint flags)
{
VkCode = vkCode;
Flags = flags;
}
public int VkCode { get; }
public uint Flags { get; }
}
private delegate nint HookProc(int nCode, nint wParam, nint lParam);
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public nint dwExtraInfo;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern nint SetWindowsHookEx(int idHook, HookProc lpfn, nint hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(nint hhk);
[DllImport("user32.dll")]
private static extern nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint GetModuleHandle(string lpModuleName);
}
まとめ
グローバルフック
- システム全体のイベントを取得
SetWindowsHookExdwThreadId = 0
キーボードフック
WH_KEYBOARD_LL- 低レベル取得
- DLL不要
- 仮想キーコード取得可能