Tutorial 24: Windows Hooks

このチュートリアルではウィンドウフックについて学ぶ。 ウィンドウフックというのは非常に強力で、これにより、他のプロセスをフックすることができ、それらのふるまいを変更することもできる。
 ソース   リソース   インクルード 
 DLLソース   DLL定義ファイル   実行結果 

Theory:

今回説明する「ウィンドウフック」はWindowsで最も強力な特徴の1つとなりうる。 これを使えば、自分の作ったプログラムのプロセス、もしくは、 他のプロセスに発生したイベントをフック(奪い取ること)できる。 「フックする」ことにより、監視したいイベントが発生したときはいつでも、 指定したフックプロシージャと呼ばれるフィルタ関数(イベントをフィルタリングするためこう呼ばれる) をWindowsにCALLしてもらえるようになる。 フックには「ローカル」と「リモート」の2種類がある。

フックをしこむと、システムのパフォーマンスに影響することを忘れてはならない。 「システムワイド」方式は特に要注意だ。 全てのイベントに対してフィルタ関数をCALLするので、著しくパフォーマンスが低下するだろう。 なので、システムワイドフックを使用するのなら、慎重に使用し、 必要の無いメッセージはフックしないようにする必要がある。 そして、他のプロセスに対してある意味余計なことをすることになるので、 そのプロセスがクラッシュする可能性は高くなる。 もし、フィルタ関数に何か間違いがあれば、そのプロセスは「落ちて」しまうだろう。 諸刃の剣だということを覚えておこう。

ウィンドウフックを使用する前に、これがどのように機能するかを先に理解しなければならない。 フックをしこんだとき、フックに関する情報が含まれたデータ構造をメモリ上に作成し、既存のフックリストに追加する。 新たなフックは古いフックの前に加えられるので、 何かイベントが発生したときに、ローカルフックをしこんでおけば、そのプロセスのフィルタ関数がCALLされる。 これは何も難しいことは無い。 問題はリモートフックの場合で、 このときはシステムが他のプロセスのアドレス空間にフックプロシージャのコードを埋め込むのであるが、 その関数はDLLになければならない。

ただし、2つの例外がある。ジャーナルレコードとジャーナルプレイバックフックだ。 それら2つのフックのプロシージャはフックをしこむスレッドになければならない。 それは、両方のフックが低レベルなハードウェア割り込み処理を扱うからだ。 そのハードウェア入力イベントは順番どおりに記録され、そして再生しなければならない。 これら2つのフックのコードがDLLにあれば、入力イベントは複数のスレッドに分散し、 ハードウェア入力イベントの順番を知るのは不可能になる。 そのため、これら2つのフックプロシージャは単一スレッド、 つまりフックをインストールするスレッドでなければならない。

フックは以下に示す14種類がある。

今度はフックのインストールとアンインストールの方法についての話に移る。 フックをしこむためには、SetWindowsHookEx関数をCALLしなければならない。

SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD

関数が成功すれば、eaxレジスタにフックハンドルが戻り値としてセットされる。 失敗すればNULLとなっている。 後でアンフックするためにこのハンドルを保存しておく必要がある。

フックを解除するにはUnhookWindowsHookEx関数をCALLする。 この関数はアンフックしたいフックのハンドルを引数にとる。 呼び出しが成功すれば、eaxレジスタに非0の値が返ってくるが、失敗すればNULLが返る。

これでフックのインストールとアンインストールの方法がわかったはずなので、 フックプロシージャについて説明しよう。

フックプロシージャはインストールしたフックのタイプに関連したイベントが発生すれば毎回CALLされる。 例えば、WH_MOUSEフックをインストールしたら、マウスイベントが発生したときにフックプロシージャがCALLされる。 フックプロシージャのプロトタイプは以下のように決まっており、インストールしたフックのタイプには関係ない。

HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD

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

リモートフックに関しては重要なことがある。 フックプロシージャは他のプロセスにマップされた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属性」は共有セクションを意味する。

Example:

今回は2つのコードがある。1つはメインプログラムでGUIパートを、もう1つはDLLでフックのインストール、アンインストールを行う。


main program

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG                  equ 101
IDC_CLASSNAME             equ 1000
IDC_HANDLE                    equ 1001
IDC_WNDPROC                equ 1002
IDC_HOOK                        equ 1004
IDC_EXIT                          equ 1005
WM_MOUSEHOOK            equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
   invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
   LOCAL hLib:DWORD
   LOCAL buffer[128]:byte
   LOCAL buffer1[128]:byte
   LOCAL rect:RECT
   .if uMsg==WM_CLOSE
       .if HookFlag==TRUE
           invoke UninstallHook
       .endif
       invoke EndDialog,hDlg,NULL
   .elseif uMsg==WM_INITDIALOG
       invoke GetWindowRect,hDlg,addr rect
       invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
   .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
   .elseif uMsg==WM_COMMAND
       .if lParam!=0
           mov eax,wParam
           mov edx,eax
           shr edx,16
           .if dx==BN_CLICKED
               .if ax==IDC_EXIT
                   invoke SendMessage,hDlg,WM_CLOSE,0,0
               .else
                   .if HookFlag==FALSE
                       invoke InstallHook,hDlg
                       .if eax!=NULL
                           mov HookFlag,TRUE
                           invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
                       .endif
                   .else
                       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
                   .endif
               .endif
           .endif
       .endif
   .else
       mov eax,FALSE
       ret
   .endif
   mov eax,TRUE
   ret
DlgFunc endp

end start


DLL

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
   .if reason==DLL_PROCESS_ATTACH
       push hInst
       pop hInstance
   .endif
   mov eax,TRUE
   ret
DllEntry Endp

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

InstallHook proc hwnd:DWORD
   push hwnd
   pop hWnd
   invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
   mov hHook,eax
   ret
InstallHook endp

UninstallHook proc
   invoke UnhookWindowsHookEx,hHook
   ret
UninstallHook endp

End DllEntry


makefile

NAME=mousehook
$(NAME).dll: $(NAME).obj
       Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
       ml /c /coff /Cp $(NAME).asm

Analysis:

今回のサンプルでは、クラス名、ウィンドウハンドル、 マウスカーソルに重なっているウィンドウのウィンドウプロシージャのアドレスが入る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 endp

InstallHook関数はとてもシンプルだ。 将来使うであろうウィンドウハンドルを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

メインウィンドウは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は正確に機能しないだろう。


[戻る]