IAT書き換えの絡みでHOOKを使いますが、その前にHOOKってどうやって使うのかサッパリだったのでMSDNで軽く調べ、サンプルを作って遊んでみました。
HOOKにはglobal hook(同じデスクトップで動作する全てのスレッドに対してHOOK)と、thread-specific hook(特定のスレッドに対してのみHOOK)がありますが、今回はglobal hookのみ取りあげます。
参考MSDNは、2010/6月末時点で以下の階層になります。
今回のサンプルは3つのEXE/DLLより構成されます。
今回は、hook.dllでキーボードイベントをHOOKします。何かキーが押されたら/離されたら、そのキーコードをメッセージボックスで表示してみます。
foo.exeは後述するようにウインドウを表示するだけのEXEです。キーボードイベントは実装しませんので、HOOK前はキーボードを押しても無反応です。しかしHOOK後は、hook.dllのHOOK本体によりキーコードがメッセージボックスで表示されるようになるはずです。
最後のhookctrl.exeはHOOKのON/OFFを切り替えます。SetWindowsHookEx()とUnhookWindowsHookEx()を呼びます。簡単なCUIベースにしても良かったのですが、今回HOOKするのはキーボードイベントです。global-hook、つまりhookctrl.exe自身もHOOK対象になりますので、CUIベースですとON/OFFをキーボードで入力するたびにHOOKが動作してしまいます。それはさすがに煩わしいので、マウスクリックだけでON/OFFできるように簡単なダイアログボックスを使ったGUIアプリにしています。
では、各EXE/DLLの内容とコンパイル方法を紹介します。
foo.exeを構成するファイルは1つだけです。
foo.c:
#include <windows.h> #define MYWNDCLSNAME "MyWindowClass" LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wndcls; HWND hWnd; MSG msg; ZeroMemory(&wndcls, sizeof(wndcls)); wndcls.lpfnWndProc = WndProc; wndcls.hInstance = hInst; wndcls.hIcon = LoadIcon(0, IDI_APPLICATION); wndcls.hCursor = LoadCursor(0, IDC_ARROW); wndcls.hbrBackground = (HBRUSH)COLOR_BACKGROUND; wndcls.lpszClassName = MYWNDCLSNAME; if (0 == RegisterClass(&wndcls)) { return -1; } hWnd = CreateWindow( MYWNDCLSNAME, "My Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hInst, NULL); if (0 == hWnd) { return -2; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while (GetMessage(&msg, 0, 0, 0)) { DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_LBUTTONDOWN: MessageBox(hWnd, "Hello, World", "Message", MB_OK | MB_ICONINFORMATION); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hWnd, uMsg, wParam, lParam); }
何もないウインドウを表示し、左クリックされたら"Hello, World"をメッセージボックスで表示するだけのWindowsプログラミング入門用サンプルです。
コンパイル方法:
> cl foo.c user32.lib
hook.dllを構成するファイルは以下の2つです。HOOK本体の関数は、hookctrl.exeから見える必要があるので、hook.defで明示的にエキスポートしています。"__declspec(dllexport)"を使う場合はhook.defは不要です。
hook.c:
#include <windows.h> #include <stdio.h> // キーボードHOOK本体 LRESULT CALLBACK MyKeyboardProc(int code, WPARAM wParam, LPARAM lParam) { char buf[200]; _snprintf(buf, 200, "virtual-key code is 0x%08X\n", wParam); MessageBox(NULL, buf, buf, MB_OK); return CallNextHookEx(NULL, code, wParam, lParam); } BOOL WINAPI DllMain( HINSTANCE hInst, DWORD dwReason, LPVOID lpvReserved) { DWORD dwPid; char buf[200]; dwPid = GetCurrentProcessId(); switch (dwReason) { case DLL_PROCESS_ATTACH: _snprintf(buf, 200, "attached to process id [0x%08X].", dwPid); MessageBox(NULL, buf, buf, MB_OK); break; case DLL_PROCESS_DETACH: _snprintf(buf, 200, "de-attached to process id [0x%08X].", dwPid); MessageBox(NULL, buf, buf, MB_OK); break; } return TRUE; }
DllMain()で、プロセスにattachされたりdetachされる時に、プロセスIDをメッセージボックスで表示するようにしています。こうすると実際にHOOKが発動してhook.dllがプロセスにattachされた瞬間を知ることができます。
hook.def:
LIBRARY hook.dll EXPORTS MyKeyboardProc
コンパイル方法:
> cl /LD hook.c user32.lib /link /def:hook.def
ダイアログボックスを使う=リソースを使いますので、次の3ファイル構成となります。
resource.h:
#ifndef IDC_STATIC #define IDC_STATIC (-1) #endif #define IDD_DIALOG1 100 #define ID_HOOK_ON 1002 #define ID_HOOK_OFF 1003
hookctrl.rc:
// Generated by ResEdit 1.4.13 // Copyright (C) 2006-2010 // http://www.resedit.net #include <windows.h> #include <commctrl.h> #include <richedit.h> #include "resource.h" // // Dialog resources // LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL IDD_DIALOG1 DIALOG 0, 0, 121, 47 STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | \ DS_SHELLFONT | WS_VISIBLE | WS_BORDER | WS_CAPTION | \ WS_DLGFRAME | WS_SYSMENU CAPTION "hook controller" FONT 8, "Ms Shell Dlg 2" { DEFPUSHBUTTON "Hook ON", ID_HOOK_ON, 12, 23, 33, 14 PUSHBUTTON "Hook OFF", ID_HOOK_OFF, 58, 23, 36, 14 LTEXT "WH_KEYBORD Hook Controller", IDC_STATIC, 11, 6, 98, 8, SS_LEFT }
hookctrl.c:
#include <windows.h> #include "resource.h" LRESULT __declspec(dllimport) CALLBACK MyKeyboardProc(int, WPARAM, LPARAM); HHOOK hHook; BOOL CALLBACK DialogProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_COMMAND: switch (LOWORD(wParam)) { case ID_HOOK_ON: hHook = SetWindowsHookEx( WH_KEYBOARD, MyKeyboardProc, GetModuleHandle("hook.dll"), 0); if (NULL == hHook) { MessageBox(hWnd, "failed.", "hook on", MB_OK); } else { MessageBox(hWnd, "success.", "hook on", MB_OK); } return TRUE; case ID_HOOK_OFF: if (!UnhookWindowsHookEx(hHook)) { MessageBox(hWnd, "failed.", "hook off", MB_OK); } else { MessageBox(hWnd, "success.", "hook off", MB_OK); } return TRUE; case IDCANCEL: EndDialog(hWnd, wParam); return TRUE; } } return FALSE; } int main(int argc, char *argv[]) { HWND hWndDesktop = NULL; HINSTANCE hCurrentInst = NULL; UINT_PTR uDlgRet = 0; hCurrentInst = GetModuleHandle(NULL); hWndDesktop = GetDesktopWindow(); DialogBox(hCurrentInst, MAKEINTRESOURCE(IDD_DIALOG1), hWndDesktop, (DLGPROC)DialogProc); return 0; }
コンパイル方法:
> rc hookctrl.rc > cl hookctrl.c hookctrl.res user32.lib hook.lib
foo.exeをOllyDbg上などで動かしてみると、HOOK ON/OFFに応じてhook.dllがメモリ内に読み込まれ、解放される様子を眺めることも出来ます。
ところで、今回の実験では同じデスクトップ上の全てのスレッドに対してHOOKされてしまいます。
次回は、HOOK対象のプロセスIDを指定出来るよう改造してみましょう。