このチュートリアルではウィンドウフックについて学ぶ。 ウィンドウフックというのは非常に強力で、これにより、他のプロセスをフックすることができ、それらのふるまいを変更することもできる。
ソース | リソース | インクルード |
DLLソース | DLL定義ファイル | 実行結果 |
|
今回説明する「ウィンドウフック」はWindowsで最も強力な特徴の1つとなりうる。 これを使えば、自分の作ったプログラムのプロセス、もしくは、 他のプロセスに発生したイベントをフック(奪い取ること)できる。 「フックする」ことにより、監視したいイベントが発生したときはいつでも、 指定したフックプロシージャと呼ばれるフィルタ関数(イベントをフィルタリングするためこう呼ばれる) をWindowsにCALLしてもらえるようになる。 フックには「ローカル」と「リモート」の2種類がある。
- ローカル
自分のプロセスで発生したイベントをフックする- リモート
他のプロセスで発生したイベントをフックする。リモートにも2つの種類がある
- スレッド指定
他のプロセスの指定したスレッドに発生したイベントをフックする。つまり、特定のプロセス内の特定のスレッド中で発生したイベントを監視する- システムワイド
システム中の全プロセスにおける全スレッドで発生する全イベントをトラップするフックをしこむと、システムのパフォーマンスに影響することを忘れてはならない。 「システムワイド」方式は特に要注意だ。 全てのイベントに対してフィルタ関数をCALLするので、著しくパフォーマンスが低下するだろう。 なので、システムワイドフックを使用するのなら、慎重に使用し、 必要の無いメッセージはフックしないようにする必要がある。 そして、他のプロセスに対してある意味余計なことをすることになるので、 そのプロセスがクラッシュする可能性は高くなる。 もし、フィルタ関数に何か間違いがあれば、そのプロセスは「落ちて」しまうだろう。 諸刃の剣だということを覚えておこう。
ウィンドウフックを使用する前に、これがどのように機能するかを先に理解しなければならない。 フックをしこんだとき、フックに関する情報が含まれたデータ構造をメモリ上に作成し、既存のフックリストに追加する。 新たなフックは古いフックの前に加えられるので、 何かイベントが発生したときに、ローカルフックをしこんでおけば、そのプロセスのフィルタ関数がCALLされる。 これは何も難しいことは無い。 問題はリモートフックの場合で、 このときはシステムが他のプロセスのアドレス空間にフックプロシージャのコードを埋め込むのであるが、 その関数はDLLになければならない。
ただし、2つの例外がある。ジャーナルレコードとジャーナルプレイバックフックだ。 それら2つのフックのプロシージャはフックをしこむスレッドになければならない。 それは、両方のフックが低レベルなハードウェア割り込み処理を扱うからだ。 そのハードウェア入力イベントは順番どおりに記録され、そして再生しなければならない。 これら2つのフックのコードがDLLにあれば、入力イベントは複数のスレッドに分散し、 ハードウェア入力イベントの順番を知るのは不可能になる。 そのため、これら2つのフックプロシージャは単一スレッド、 つまりフックをインストールするスレッドでなければならない。
フックは以下に示す14種類がある。
- WH_CALLWNDPROC
SendMessage関数が呼び出されたときにCALLされる- WH_CALLWNDPROCRET
SendMessage関数から返ってきたときにCALLされる- WH_GETMESSAGE
GetMessage関数かPeekMessage関数がCALLされたときにCALLされる- WH_KEYBOARD
GetMessage関数、もしくはPeekMessage関数がメッセージキューからWM_KEYUPかWM_KEYDOWNを取得したときにCALLされる- WH_MOUSE
GetMessage関数、もしくはPeekMessage関数がメッセージキューからマウスメッセージを取得したときにCALLされる- WH_HARDWARE
GetMessage関数、もしくはPeekMessage関数がメッセージキューから関与しないハードウェアメッセージを取得したときにCALLされる- WH_MSGFILTER
ダイアログボックス、メニュー、スクロールバーがまさにメッセージを処理しようとした瞬間にCALLされる。このフックはローカル限定で、内部でメッセージループを行っているオブジェクト固有のものである。- WH_SYSMSGFILTER
システムワイドであるという点を除いてWH_MSGFILTERと同じ- WH_JOURNALRECORD
Windowsがハードウェア入力キューからメッセージを取得したときにCALLされる- WH_JOURNALPLAYBACK
Windowsがシステムのハードウェア入力キューからメッセージを取得したときにCALLされる- WH_SHELL
タスクバーの再描画が必要になったときなどのような、シェルに関して何か発生したときにCALLされる- WH_CBT
コンピュータを利用したトレーニング(CBT)の際に使用する- WH_FOREGROUNDIDLE
Windowsが内部的に使用する。一般的にはほとんど使用されない- WH_DEBUG
フックプロシージャをデバッグする際に使用する今度はフックのインストールとアンインストールの方法についての話に移る。 フックをしこむためには、SetWindowsHookEx関数をCALLしなければならない。
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
- HookType
上のリストで紹介されている、値WH_MOUSEやWH_KEYBOARDの1つ- pHookProc
指定したフックのメッセージを処理するためにCALLするフックプロシージャのアドレス。フックがリモートの場合、DLLにプロシージャが無いといけない。リモートでなければ、プロセス内になければならない。- hInstance
フックプロシージャが入っているDLLのインスタンスハンドル。ローカルフックならこれはNULLでなければならない。- ThreadID
フックをしこみたいスレッドID。このパラメータにより、フックがローカルかリモートかを判別する。これがNULLなら、Windowsはシステム全体のスレッドに影響するシステムワイドリモートフックとして解釈する。自分のプロセスのスレッドIDを指定すると、ローカルフックとなる。もし他のプロセスのスレッドIDを指定すると、スレッド特有のリモートフックとなる。
このルールには2つの例外がある。WH_JOURNALRECORDとWH_JOURNALPLAYBACK は常に、ローカルシステムワイドフックで、DLL内にある必要は無い。そして、WH_SYSMSGFILTERは常にリモートワイドフックである。これは、ThreadID==0として、WH_MSGFILTERを指定したものと同一である。関数が成功すれば、eaxレジスタにフックハンドルが戻り値としてセットされる。 失敗すればNULLとなっている。 後でアンフックするためにこのハンドルを保存しておく必要がある。
フックを解除するにはUnhookWindowsHookEx関数をCALLする。 この関数はアンフックしたいフックのハンドルを引数にとる。 呼び出しが成功すれば、eaxレジスタに非0の値が返ってくるが、失敗すればNULLが返る。
これでフックのインストールとアンインストールの方法がわかったはずなので、 フックプロシージャについて説明しよう。
フックプロシージャはインストールしたフックのタイプに関連したイベントが発生すれば毎回CALLされる。 例えば、WH_MOUSEフックをインストールしたら、マウスイベントが発生したときにフックプロシージャがCALLされる。 フックプロシージャのプロトタイプは以下のように決まっており、インストールしたフックのタイプには関係ない。
HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
- nCode
フックコードを指定する- wParam and lParam
イベントについての追加情報を指定するHookProcという関数名は、実際のところはなんでも好きなものにできる。 wParamとlParam、nCodeの解釈はインストールしたフックのタイプにより異なっており、 戻り値も同様である。例を以下に示す。
WH_CALLWNDPROC
- nCode
ウィンドウに送信されるメッセージがある、ということを意味するHC_ACTIONしか指定できない- wParam
0でなければ、送信されるメッセージ。- lParam
CWPSTRUCT構造体へのポインタ- 戻り値
使用しない。0が返ってくる。
WH_MOUSE
- nCode
HC_ACTION もしくは HC_NOREMOVE- wParam
マウスメッセージ- lParam
MOUSEHOOKSTRUCT構造体へのポインタ- 戻り値
メッセージが処理されれば0。メッセージが破棄されれば1。インストールしたいフックの戻り値や引数の意味はWin32APIリファレンスにより、 詳細は自分で調査しなければならない。
ここで、フックプロシージャについてちょっとした問題がある。 先ほど説明したが、インストールされたフックは、 一番最近のものがリンクリストの一番最初に配置されるということを覚えているだろうか。 イベントが発生したとき、Windowsはそのリストの一番最初のフックだけをCALLする。 なので、本来呼ばれるはずである次のフックをCALLする責任がある。 もちろん次のフックを呼ばない選択もできるが、CALLしたほうが無難だ。 CallNextHookEx関数をCALLすることにより次のフックをCALLすることができる。
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
- hHook
自分のフックのハンドル。このハンドルを使用して、フックプロシージャリンクリストをなめて、次にCALLするフックプロシージャを検索する。- nCode, wParam and lParam
Windowsから受け取った値を、そのままCallNextHookEx関数へ渡すリモートフックに関しては重要なことがある。 フックプロシージャは他のプロセスにマップされたDLL内に実行コードがなければならない。 WindowsがDLLを他のプロセスにマップするとき、 dataセクションは他のプロセスにマップしないのである。 つまり、全てのプロセスは実行コードのただ1つのコピーを共有しているが、 DLLのdataセクションに関しては個々にコピーを持つことになる!
あなたはきっと、DLLのdataセクションの変数に何か値を格納したとき、 そのDLLをロードしている全てのプロセスでその値を共有しているものだと考えているかもしれないが、 それは間違っている。 しかし通常は、それぞれのプロセスがDLLを独自にコピーしているという錯覚を与えるよう動作するため、 この動作は望ましいものである。しかし、ウィンドウフックを考えるときはそうではない。 プログラマとしては、DLLがデータも含めて全てのプロセスにおいて識別できるようになっていてほしいのである。
この解決策は、dataセクションを共有するようにマークすることだ。 リンカにセクション属性を指定すればよい。 MASMでは以下のように記述する。
/SECTION:<section name>, S初期化dataセクション名は .data で、非初期化データは .bss である。 例えば、フックプロシージャ付きのDLLをアセンブルしたい場合、 そのDLLが全プロセス間で共有する非初期化データを保持したければ、
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........というように記述する。「S属性」は共有セクションを意味する。
|
今回は2つのコードがある。1つはメインプログラムでGUIパートを、もう1つはDLLでフックのインストール、アンインストールを行う。
|
|
|
|
今回のサンプルでは、クラス名、ウィンドウハンドル、 マウスカーソルに重なっているウィンドウのウィンドウプロシージャのアドレスが入る3つのエディットコントロールのあるダイアログボックスが表示される。 そして、「Hook」と「Exit」の2つのボタンがある。 Hookボタンを押すと、プログラムはマウスの入力をフックし、Hookの文字列をUnhookに変更する。 マウスカーソルをウィンドウに重ねると、 そのウィンドウに関する情報がメインウィンドウのエディットボックスに表示される。 それから、Unhookボタンを押すと、プログラムはマウスフックを解除する。
メインプログラムはメインウィンドウとしてダイアログボックスを使用しており、 そのメインウィンドウとフックDLLとの間でやりとりを行うために使用するカスタムメッセージ、 WM_MOUSEHOOKを定義している。 メインプログラムがこのメッセージを受け取ると、 wParamにはマウスカーソルが重なっているウィンドウのハンドルが格納されている。 もちろん、これは変更が可能なのだが、今回は単純にするためそうしている。 また、メインウィンドウとフックDLLとの間で行うコミュニケーションは独自の方法を使用してかまわない。
.if HookFlag==FALSE invoke InstallHook,hDlg .if eax!=NULL mov HookFlag,TRUE invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText .endifプログラムではフックの状態をモニタリングするため、HookFlagを監視している。 フックがインストールされていなければ、FALSEとなり、インストールされていればTRUEとなる。
ユーザがフックボタンを押すと、プログラムはフックが既にインストールされているかどうかチェックする。 もしインストールされていなければ、フックDLLにあるInstallHook関数をCALLしてインストールする。 この関数には引数としてメインダイアログのハンドルを渡すので、 フックDLLはWM_MOUSEHOOKメッセージをこのメインウィンドウにきちんと送信する。
プログラムがロードされれば、フックDLLもロードされる。 実際、DLLはプログラムがメモリにロードされた後すぐにロードされる。 DLLのエントリポイント関数はメインプログラムの最初の命令がまさに実行される直前にCALLされる。 そのため、メインプログラムが実行されるときにはDLLの初期化は済んでいることになる。 ちなみに、フックDLLのエントリポイント関数のコードは以下のようなものとなる。
.if reason==DLL_PROCESS_ATTACH push hInst pop hInstance .endifこのコードは単に、フックDLLのインスタンスハンドルを、 InstallHook関数で使用するhInstanceというグローバル変数に保存している。 DLLのエントリポイント関数はDLL内の他の関数より先にCALLされるので、 hInstanceは絶対に有効な値となる。
コードではhInstanceを .data セクションに置き、個々のプロセス毎に別々に保存される値としている。 マウスカーソルがあるウィンドウに重なったとき、フックDLLはプロセスにマッピングされる。 ここで、本来そのフックDLLがロードされる予定だったアドレスに他のDLLが既にロードされていたとしよう。 そうすると、もちろんそのフックDLLは違うアドレスに再マップされ、 hInstanceは新しくロードされたアドレスに変更される。 ユーザがUnhookボタンを押し、それからHookボタンを押すと、SetWindowsHookEx関数が再度CALLされる。 しかしながら、このとき、インスタンスハンドルとして新しいアドレスが使用されるのだが、 そのインスタンスアドレスは不正なものとなっている。 なぜなら、この例のプロセスでは、フックDLLのロードアドレスは変わっていないのである。 フックは自分の作成したウィンドウに発生するマウスイベントだけをフックするという場合、 ローカルなものとなり、ほとんど価値のないもになる。(このセンテンスちょっと意味不明・・・=p)
InstallHook proc hwnd:DWORD push hwnd pop hWnd invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL mov hHook,eax ret InstallHook endpInstallHook関数はとてもシンプルだ。 将来使うであろうウィンドウハンドルをhWndというグローバル変数にセーブしている。 そして、マウスフックをインストールするために、SetWindowsHookEx関数をCALLしている。 SetWindowsHookEx関数の戻り値は、UnhookWindowsHookEx関数で使用するために、 hHookというグローバル変数に格納される。
SetWindowsHookEx関数をCALLした後、マウスフックが動作するようになる。 システムにマウスイベントが発生したら、フックプロシージャのMouseProc関数がCALLされる。
MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD invoke CallNextHookEx,hHook,nCode,wParam,lParam mov edx,lParam assume edx:PTR MOUSEHOOKSTRUCT invoke WindowFromPoint,[edx].pt.x,[edx].pt.y invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0 assume edx:nothing xor eax,eax ret MouseProc endp一番初めにすることは、CallNextHookEx関数をCALLして他のフックにマウスイベントを処理する機会を提供することだ。 その後、WindowFromPoint関数をCALLして指定したスクリーン座標値に位置するウィンドウのハンドルを取得する。 ここで、現在のマウス座標値として引き渡される lParamによりポイントされているMOUSEHOOKSTRUCT構造体にあるPOINT構造体を使用していることに注意せよ。 それから、ウィンドウハンドルをWM_MOUSEHOOKメッセージとともにPostMessage関数をCALLすることにより、 メインプログラムに送信している。 ここで1つ覚えておかなければならないことがある。 それは、フックプロシージャ内でSendMessage関数をCALLしてはいけないということだ。 CALLしてしまうと、メッセージがデッドロック状態になる可能性があるので、 PostMessage関数を使用する。
MOUSEHOOKSTRUCT構造体は以下のようになっている。
MOUSEHOOKSTRUCT STRUCT DWORD pt POINT <> hwnd DWORD ? wHitTestCode DWORD ? dwExtraInfo DWORD ? MOUSEHOOKSTRUCT ENDS
- pt
現在指しているマウスカーソルのスクリーン座標値- hwnd
マウスメッセージを受け取るウィンドウのハンドル。たいていはマウスカーソルが重なっているウィンドウなのだが、常にそうであるわけではない。ウィンドウがSetCapture関数をCALLすれば、マウス入力はそのウィンドウにリダイレクトされる。このため、このメンバ変数は使用しないが、WindowFromPoint関数をCALLするという選択もある。- wHitTestCode
ヒットテストの値を指定する。ヒットテスト値には現在のマウスカーソルの位置についてより詳しい情報が入っており、マウスカーソルがウィンドウのどの部分にあるかがわかる。詳細は、WM_NCHITTESTメッセージについてWin32APIリファレンスを参照すること。- dwExtraInfo
メッセージにさらなる情報を付け加えるのに使用する変数。通常、mouse_event関数によってセットされ、GetMessageExtraInfo関数により取得する。メインウィンドウはWM_MOUSEHOOKメッセージを受け取ると、 wParamに格納されているウィンドウハンドルを使用してウィンドウについての情報を取得する。
.elseif uMsg==WM_MOUSEHOOK invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128 invoke wsprintf,addr buffer,addr template,wParam invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0 invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer .endif invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128 invoke GetClassName,wParam,addr buffer,128 invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0 invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer .endif invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128 invoke GetClassLong,wParam,GCL_WNDPROC invoke wsprintf,addr buffer,addr template,eax invoke lstrcmpi,addr buffer,addr buffer1 .if eax!=0 invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer .endifちらつき防止のため、エディットコントロールにある文字列は既に入っているものと、 今マウスがポイントしているウィンドウに関する情報の文字列が同一のものかどうかチェックして、 もし同一なら何もしない。
GetClassName関数をCALLしてクラス名を、 GetClassLong関数をGCL_WNDPROCを指定してウィンドウプロシージャのアドレスを取得し、 文字列を整形してエディットコントロールに書き込む。
invoke UninstallHook invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText mov HookFlag,FALSE invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULLユーザがUnhookボタンを押すと、フックDLLにあるUninstallHook関数がCALLされる。 UninstallHook関数は単にUnhookWindowsHookEx関数をCALLしているだけで、 その後、ボタンに表示されている文字列を「Hook」に変更し、HookFlagをFALSEにして、 エディットコントロールの文字列をクリアする。
Makefileのリンカオプションで一つ気をつけなければならないことがある。
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS全プロセスがフックDLL内の同じ非初期化データを使いまわすために、 共有セクションとして、.bss セクションを指定している。 このオプションが無いと、フックDLLは正確に機能しないだろう。