グローバルフック(またはシステムフック)を使ってWindows APIで遊んでみました。
フックというのは
引用元: wikipedia
主に元のプログラムに対する機能追加・拡張やカスタマイズの手段として使われるほか、デバッグのための情報収集にも有効である。このような有用な使い方の反面、既存のプログラムの動作を変更できることから、悪意を持ったプログラムによって利用される場合もある。例えばOSのキー入力処理のフックを使えばキーロガーを実装できることになる。
というものです。
メッセージを横取りして処理してしまうってやつです。
本格的なキーロガーだとおそらく横取りした後に気付かれないように
ちゃんとまたメッセージを送りなおすんでしょうけど…
ということで、キーボード入力をOS全体でフックしてみました。
これを行うにはDLL化しなくてはなりません(しなくてもいい方法はあるにはある)。
ちなみにこれが容易にできるのはC++だけです。
C#でもP/Invokeを使えばできるようですけど……。
ちなみにDLLにするのはイイのですが注意点が一つ。
共有セグメントでデータ共有をしなくてはなりません。
簡単に言ってしまうとグローバル変数の共有ですね。
メモリマップドファイルでもできるのですがお手軽な方法を採用しました。
とりあえず説明するより作ってみたほうが速いということで
作成の仕方をざっくり載せます。
使うのはWindows APIです。MFC使うまでもないです。
また今回はほとんどのコードがCです。
作るもの
たとえ作成したウィンドウがアクティブになっていなくても
どこでもキーボードをおしたら、メッセージボックスで出すといううざいもの。
ただし、エンターだけは除外してます。
あれ…メッセージボックスのタイトルがエラーになってるけど、このままにしておきます。
作り方
C++のソリューション&プロジェクトを作成します。
Visual Studioを立ち上げます。
新規作成 > プロジェクト
で新規プロジェクトを作成します。
テンプレートをC++のWin32プロジェクトを選択します。
名前は何でもいいですがGlobalHookTestとでもしましょう。
Win32アプリケーションのウィザード
めんどいんでスクショで
このようにするとWindowsのスケルトンができます。
ビルドして実行すると殺風景な無駄にでかいWindowが出来上がります。
ビルドしてエラーがないことを確認します。
DLLを作成します
ソリューションエクスプローラーでソリューションを選択して右クリック
追加 > 新しいプロジェクト
Win32プロジェクトを選択して
GlobalHookDllとでも名前を付けましょう
今度はDLLを選択します。
DLLのソースコードを作成します
暗黙のリンクでやっちまいます。
ソリューションエクスプローラーから右クリックで追加で
GlobalHookDll.hを作成して、次のコードをコピペします
extern "C"
を付けるかどうかはお好みで。
ネームマングリングの部分が消えるかどうかの違いです。
Cで使いたいならextern "C"
しましょう
dllmain.cppを消します
自動生成されているdllmain.cppがありますが邪魔なので消します。
GlobalHookDll.h
#pragma once #include "stdafx.h" #ifdef EXPORT_ #define EXPORT_API_ __declspec(dllexport) #else #define EXPORT_API_ __declspec(dllimport) #endif EXPORT_API_ LRESULT CALLBACK KeyHookProc(int, WPARAM, LPARAM); EXPORT_API_ int SetHook(HWND hWnd); EXPORT_API_ int ResetHook();
GlobalHookDll.cpp
つづいてHookの処理を書きます。
スケルトンができていますが、かまわずコードを書き換えちまいましょう。
// GlobalHookDll.cpp : DLL アプリケーション用にエクスポートされる関数を定義します。 // // see: http://chaboneko.daiwa-hotcom.com/wordpress/?p=34 #include "stdafx.h" #include <cstdio> #include <tchar.h> #include "GlobalHookDll.h" // すべてのスレッドにセットされるフックになるので // グローバル変数を共有する必要がある // 共有セグメント #pragma data_seg(".shareddata") HHOOK hKeyHook = 0; HWND g_hWnd = 0; // キーコードの送り先のウインドウハンドル #pragma data_seg() HINSTANCE hInst; EXPORT_API_ int SetHook(HWND hWnd) { hKeyHook = SetWindowsHookEx(WH_KEYBOARD, KeyHookProc, hInst, 0); if (hKeyHook == NULL) { // フック失敗 } else { // フック成功 g_hWnd = hWnd; } return 0; } EXPORT_API_ int ResetHook() { if (UnhookWindowsHookEx(hKeyHook) != 0) { // フック解除成功 } else { // フック解除失敗 } return 0; } EXPORT_API_ LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wp, LPARAM lp) { TCHAR msg[64] = { 0 }; if (nCode < 0) // 決まり事 return CallNextHookEx(hKeyHook, nCode, wp, lp); if (nCode == HC_ACTION) { //目的のウインドウにキーボードメッセージと、キーコードの転送 // どこで押してもHOOKする // ボタンが押された状態の時限定(離しはスルー) if ((lp & 0x80000000) == 0) { // 通常キー if ((lp & 0x20000000) == 0) { // Enter以外 if (wp != VK_RETURN) { _stprintf_s(msg, _T("%cが押されたよ(´・ω・`)!"), int(wp)); MessageBox(NULL, msg, NULL, MB_OK); PostMessage(g_hWnd, WM_KEYDOWN, wp, 0); } } // システムキー(Alt(+何か)、もしくはF10の時) else { MessageBox(NULL, TEXT("システムキーが押されたよ!"), NULL, MB_OK); PostMessage(g_hWnd, WM_SYSKEYDOWN, wp, 0); } } } return CallNextHookEx(hKeyHook, nCode, wp, lp); } // エントリポイント BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // アタッチ hInst = hModule; break; case DLL_PROCESS_DETACH: // デタッチ break; } return TRUE; }
defファイルを作成します
data_segのための情報が必要なので、
GlobalHookDll.defというファイルを作成します。
SECTIONS .shareddata READ WRITE SHARED
DLLのプロジェクトの設定をします
ここが一番ややこしいですが、
とりあえず僕のよくやる方法でプロジェクトを設定してしまいます。
もっとちゃんとした方法があるのかも……。
ソリューションエクスプローラーからGlobalHookDllプロジェクトを右クリックし
プロパティ
を選択します。
構成をすべての構成
にして
構成プロパティ > C/C++ > プリプロセッサ > プリプロセッサの定義
でEXPORT_;
を追加します。まぁ要はdefineしますよってことです。
次にリンカー > 入力 > モジュール定義ファイル
で
GlobalHookDll.defを入力します。※これはdefファイルを作った時点で勝手に入るので
設定不要です。
DLLをビルドします。
エラーがないことを確認します。
これでDLL側は完成です。
利用側(DLLじゃないEXEのほう)の処理を書きます
要はWinMain側の設定やらなにやらさらに必要です。
もうちょっとで完成です。
GlobalHookTest.h
これだけです。ちなみに自動生成されるstdafx.h, stdafx.cpp, targetvar.hはそのままです。
消したい場合はプリコンパイル済みヘッダの設定を変えないといけません。
#pragma once #include "resource.h"
GlobalHookTest.cpp
AddやModって書いているところが追加部分です。
// GlobalHookTest.cpp : アプリケーションのエントリ ポイントを定義します。 // #include "stdafx.h" #include "GlobalHookTest.h" #include "GlobalHookDll.h" // Add #define MAX_LOADSTRING 100 // グローバル変数: HINSTANCE hInst; // 現在のインターフェイス TCHAR szTitle[MAX_LOADSTRING]; // タイトル バーのテキスト TCHAR szWindowClass[MAX_LOADSTRING]; // メイン ウィンドウ クラス名 HWND hWnd; // Add const int ClientWidth = 260; // Add const int ClientHeight = 40; // Add HFONT hFont1; // Add HFONT hOldFont; // Add // このコード モジュールに含まれる関数の宣言を転送します: ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // TODO: ここにコードを挿入してください。 MSG msg; HACCEL hAccelTable; // グローバル文字列を初期化しています。 LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_GLOBALHOOKTEST, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // アプリケーションの初期化を実行します: if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_GLOBALHOOKTEST)); SetHook(hWnd); // Add // メイン メッセージ ループ: while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } ResetHook(); // Add return (int) msg.wParam; } // // 関数: MyRegisterClass() // // 目的: ウィンドウ クラスを登録します。 // ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_GLOBALHOOKTEST)); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = MAKEINTRESOURCE(IDC_GLOBALHOOKTEST); wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassEx(&wcex); } // // 関数: InitInstance(HINSTANCE, int) // // 目的: インスタンス ハンドルを保存して、メイン ウィンドウを作成します。 // // コメント: // // この関数で、グローバル変数でインスタンス ハンドルを保存し、 // メイン プログラム ウィンドウを作成および表示します。 // BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HWND hWnd; hInst = hInstance; // グローバル変数にインスタンス処理を格納します。 // Mod Start hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); DWORD dwStyle = WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME; // ウィンドウサイズ変更不可 RECT rc = { 0, 0, ClientWidth, ClientHeight }; AdjustWindowRectEx(&rc, dwStyle, FALSE, 0); int nWidth = rc.right - rc.left; int nHeight = rc.bottom - rc.top; hWnd = CreateWindow( szWindowClass, szTitle, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, nWidth, nHeight, NULL, NULL, hInstance, NULL ); // Mod End if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; } // // 関数: WndProc(HWND, UINT, WPARAM, LPARAM) // // 目的: メイン ウィンドウのメッセージを処理します。 // // WM_COMMAND - アプリケーション メニューの処理 // WM_PAINT - メイン ウィンドウの描画 // WM_DESTROY - 中止メッセージを表示して戻る // // LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; LPCTSTR lpszText = _T("ぐろぉばるふっく(;´・ω・)"); // Add switch (message) { case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // 選択されたメニューの解析: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: 描画コードをここに追加してください... hOldFont = static_cast<HFONT>(SelectObject(hdc, hFont1)); // Add TextOut(hdc, 0, 0, lpszText, _tcslen(lpszText)); // Add EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } // バージョン情報ボックスのメッセージ ハンドラーです。 INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: return (INT_PTR)TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return (INT_PTR)TRUE; } break; } return (INT_PTR)FALSE; } // Add Start HFONT MyCreateFont(int nHeight, DWORD dwCharSet, LPCTSTR lpName) { return CreateFont( nHeight, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, dwCharSet, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, // アンチエイリアス DEFAULT_PITCH | FF_DONTCARE, lpName ); } // Add End
EXE側のプロジェクトの設定をする
ここが一番重要かもしれません。
ヘッダーとdllとlibの位置を教えてあげる必要があります。
もしくはパスの通っているところにおいてあげればいいのですが、
場所と名前を教えてあげることにします。
GlobalHookTestを右クリックして
構成をすべての構成
になっていることを確認して
C/C++ > 全般 > 追加のインクルードディレクトリ
に
$(SolutionDir)\GlobalHookDll;
を追加してやります。
リンカー > 追加の依存ファイル
に
GlobalHookDll.lib;
を追加してやります。
リンカー > 全般 > 追加のライブラリディレクトリ
に
$(SolutionDir)\$(Configuration)
と入力します。
$(Configuration)はReleaseとDebugのことです。
リンカー > 入力 > 追加の依存ファイル
に
GlobalHookDll.lib;
を追加してやります。
ビルドする
ソリューションをビルドしてやれば正常終了するはずです。
リンクエラーが出ていたらどこかしらの設定が抜けている可能性があります。
実行
実行したら最初に示したものが出来上がるはずです