目次
- はじめに
- 1. Windowsフックのテクニック
- 2. CreateRemoteThread+LoadLibraryのテクニック
- プロセス間通信
- 3. CreateRemoteThread+WriteProcessMemoryのテクニック
- このテクニックを使ってリモートコントロールをサブクラス化する方法
- CreateRemoteThread+WriteProcessMemoryのテクニックはどこで使用すべきか
- おわりに
- 付録
- References
はじめに
CodeGuruのサイトにはパスワードスパイのチュートリアルがいくつか投稿されていますが、それらはいずれもWindowsフックを利用しています。このようなユーティリティを作成するには、他に方法はないのでしょうか? 実はあります。しかし別の方法を説明する前に、この問題について簡単に復習しておきましょう。
コントロールのコンテンツを読み取るには、それが自作アプリケーション内にある場合でもない場合でも、コントロールに対してWM_GETTEXT
メッセージを送信します。エディットコントロールに対しても同じことが言えますが、1つ例外があります。そのエディットコントロールが別のプロセスに属していて、ES_PASSWORD
スタイルが設定されている場合には、この方法は失敗します。そのパスワードコントロールを所有しているプロセスだけが、WM_GETTEXT
を通じてコントロールのコンテンツを取得できます。したがって、この問題を避けるためには、
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
という関数を別のプロセスのアドレス空間内で実行する必要があります。
一般的には、この問題を解決するには次の3つの方法が考えられます。
- コードをDLL内に組み込み、そのDLLをWindowsフックを使ってリモートプロセスにマッピングする
- コードをDLL内に組み込み、そのDLLを
CreateRemoteThread
+LoadLibrary
のテクニックを使ってリモートプロセスにマッピングする - 独立したDLLを記述するのではなく、
WriteProcessMemory
を通じてコードをリモートプロセスに直接コピーし、CreateRemoteThread
を使ってそのコードの実行を開始する(このテクニックの詳細についてはこちらを参照)
1. Windowsフックのテクニック
デモアプリケーション:HookSpy、HookInjEx
Windowsフックの主な役割は、あるスレッドのメッセージトラフィックを監視することです。一般的には、次の種類があります。
- ローカルフック - このプロセスに属する任意のスレッドのメッセージトラフィックを監視します。
- リモートフック - 次の2種類があります。
- スレッド固有 - 別のプロセスに属するスレッドのメッセージトラフィックを監視します。
- システムワイド - システム上で現在実行しているすべてのスレッドのメッセージトラフィックを監視します。
フックされる側のスレッドが別のプロセスに属している場合(上記2-1および2-2の場合)は、フックする側のプロシージャはダイナミックリンクライブラリ(DLL)内になければなりません。フックプロシージャがDLL内にあるときは、そのDLLが、フック対象スレッドのアドレス空間へとマッピングされます。このときWindowsは、フックプロシージャだけでなくDLL全体をマッピングします。この機能により、Windowsフックを利用して別のプロセスのアドレス空間にコードを割り込ませることができます。
本稿では、フックについてこれ以上詳しく説明しません(詳しく知りたい方は、MSDNのSetWindowHookEx
API関数を参照してください)。その代わりに、マニュアルには書かれていない有益なヒントを2つ紹介しておきます。
1. DLLはいつマッピングされるか
SetWindowsHookEx
の呼び出しが正常に完了すると、Windowsは指定のDLLをフック対象スレッドのアドレス空間に自動的にマッピングしますが、マッピングが即座に行われるとは限りません。Windowsフックはメッセージに関するものなので、適切なイベントが発生しない限り、DLLは実際にはマッピングされません。次に例を示します。
あるスレッドのキュー化されていないすべてのメッセージを監視するフック(WH_CALLWNDPROC
)をインストールした場合は、フック対象スレッド(のいずれかのウィンドウ)に実際にメッセージが送信されるまでは、DLLはリモートプロセスにマッピングされません。言い換えると、フック対象スレッドにメッセージが送信される前にUnhookWindowsHook
を呼び出した場合は、SetWindowsHookEx
の呼び出しが成功したとしても、DLLがリモートプロセスにまったくマッピングされないことになります。即座にマッピングを行うためには、SetWindowsHookEx
を呼び出した直後に、対象スレッドに対して適切なイベントを送信します。
UnhookWindowsHook
の呼び出し後にDLLのマッピングを解除するときにも同じことが言えます。適切なイベントが起きるまでは、DLLは実際にはマッピング解除されません。
2. パフォーマンスの低下を防ぐには
フックをインストールすると、システム全体のパフォーマンスに影響が出ることがあります(特にシステムワイドフックの場合)。しかし、DLLマッピングメカニズムとして主にスレッド固有フックを使用しており、メッセージのトラップを行っていない場合には、この弱点を簡単に克服できます。次のコード例を見てください。
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { if( ul_reason_for_call == DLL_PROCESS_ATTACH ) { // Increase reference count via LoadLibrary char lib_name[MAX_PATH]; ::GetModuleFileName( hDll, lib_name, MAX_PATH ); ::LoadLibrary( lib_name ); // Safely remove hook ::UnhookWindowsHookEx( g_hHook ); } return TRUE; }
さて、これで何が起きるでしょうか。
まず、Windowsフックを通じてこのDLLをリモートプロセスにマッピングします。その後、DLLが実際にマッピングされた直後に、UnhookWindowsHookEx
によってフックが解除されます。通常ならば、最初のメッセージがフック対象スレッドに到達するとすぐに、DLLのマッピングも解除されるはずです。しかしこの例では、LoadLibrary
を呼び出してDLLの参照カウントを増やすという「ずる」をして、このマッピング解除を防いでいます。
ここで1つ疑問が浮かんできます。終了時にはどうやってDLLをアンロードすればよいのでしょうか?このスレッドは既にフック解除されているので、UnhookWindowsHookEx
を使うことはできません。そこで、次のような方法を使用します。
- DLLをマップ解除する直前に別のフックをインストールする
- リモートスレッドに「特殊な」メッセージを送信する
- このメッセージをフックプロシージャ内でキャッチし、それへの応答として
FreeLibrary
とUnhookWindowsHookEx
を呼び出す
こうすると、リモートプロセスに対してDLLをマッピング/マッピング解除する間だけフックを使用することになるので、とりあえずフック対象スレッドのパフォーマンスに影響が出ることはありません。言い換えると、これは次に説明するLoadLibrary
テクニック(CreateRemoteThread+LoadLibraryのテクニックを参照)よりもターゲットプロセスの邪魔をしないDLLマッピングメカニズムです。また、LoadLibrary
テクニックとは異なり、このソリューションはWinNTとWin9xの両方で使用できます。
では、この方法はいつ使用すればよいのでしょうか。
たとえば、DLLを長期間リモートプロセスにマッピングしておく必要があるが(別のプロセスに属するコントロールをサブクラス化するときなど)、ターゲットプロセスの邪魔はできるだけしたくない場合は、この方法を使用します。サンプルのHookSpyアプリケーションではこの方法を使用しませんでした。このアプリケーションではDLLの使用期間が短く、パスワードを取得する間しか使用しないからです。この方法の使用例を示すためには、HookInjExという別のサンプルを用意しました。HookInjExでは、explorer.exeに対してDLLをマッピング/マッピング解除し、Windowsの[スタート]ボタンをサブクラス化しています。具体的には、[スタート]ボタンの左クリックと右クリックを入れ替えます。
本稿の最初で紹介しているダウンロードパッケージに、HookSpyおよびHookInjExの実行可能ファイルとソースファイルが含まれています。
2. CreateRemoteThread+LoadLibraryのテクニック
デモアプリケーション:LibSpy
一般的には、LoadLibrary
API関数を使用すればどんなプロセスでもDLLを動的にロードできます。しかし、外部プロセスからこの関数を呼び出すにはどうしたらいいでしょうか。答えは、CreateRemoteThread
を使用することです。
まず、LoadLibrary
とFreeLibrary
というAPI関数の宣言を見てみましょう。
HINSTANCE LoadLibrary( LPCTSTR lpLibFileName // address of filename of library module ); BOOL FreeLibrary( HMODULE hLibModule // handle to loaded library module );
これらの宣言を、CreateRemoteThread
に渡すスレッドルーチンThreadProc
の宣言と比べてみましょう。
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
見てのとおり、どの関数も同じ呼び出し規則を使用しており、いずれも32ビットパラメータを受け取ります。また、戻り値のサイズも同じです。したがって、LoadLibrary
/FreeLibrary
へのポインタを、CreateRemoteThread
にスレッドルーチンとして渡すことが可能です。
しかし、これには2つの問題があります(詳しくは下記のCreateRemoteThread
の説明を参照)。
CreateRemoteThread
のlpStartAddress
パラメータは、リモートプロセス内のスレッドルーチンの開始アドレスを表さなければなりません。lpParameter
パラメータ(ThreadFunc
に渡されるパラメータ)が通常の32ビット値として解釈される場合は(FreeLibrary
はこれをHMODULE
として解釈します)、何も問題はありません。しかし、lpParameter
パラメータがポインタとして解釈される場合は(LoadLibraryA
はこれをchar
文字列へのポインタとして解釈します)、このパラメータはリモートプロセス内の何らかのデータを指さなければなりません。
1番目の問題は、実はあらかじめ解決されています。LoadLibrary
とFreeLibray
はどちらもkernel32.dll内の関数です。kernel32.dllは必ず存在し、すべてのノーマルプロセス(付録Aを参照)内で同じロードアドレスにあることが保証されているので、LoadLibrary
/FreeLibray
のアドレスもすべてのプロセス内で同じになります。したがって、必ず有効なポインタがリモートプロセスに渡されます。
2番目の問題も、簡単に解決することができます。WriteProcessMemory
を使用して、LoadLibrary
に必要なDLLモジュール名をリモートプロセスにコピーするだけでよいのです。
したがって、CreateRemoteThread
+LoadLibrary
のテクニックを使用するには、次の手順を行います。
- リモートプロセスへのハンドルを取得します(
OpenProcess
)。 - リモートプロセス内でDLL名のためのメモリを割り当てます(
VirtualAllocEx
)。 - 割り当てたメモリにDLL名を完全パスで書き込みます(
WriteProcessMemory
)。 CreateRemoteThread
とLoadLibrary
を使用して、DLLをリモートプロセスにマッピングします。- リモートスレッドが終了するまで、つまり
LoadLibrary
の呼び出しが返ってくるまで待機します(WaitForSingleObject)
。言い換えると、DLL_PROCESS_ATTACH
によって呼び出されたDllMain
が返ってきたときに、スレッドが終了します。 - リモートスレッドの終了コードを取得します(
GetExitCodeThread
)。これはLoadLibrary
の戻り値なので、マッピングされたDLLのベースアドレス(HMODULE
)を表します。 - 手順2で割り当てたメモリを解放します(
VirtualFreeEx
)。 CreateRemoteThread
とFreeLibrary
を使用して、リモートプロセスからDLLをアンロードします。手順6で取得したHMODULE
ハンドルをFreeLibrary
に渡します(CreateRemoteThread
のlpParameter
パラメータを使用)。- スレッドが終了するまで待機します(
WaitForSingleObject
)。
また、使用し終わったらすべてのハンドルをクローズすることも忘れないでください。手順4と手順8で作成したスレッドへのハンドルと、手順1で取得したリモートプロセスへのハンドルをクローズします。
LibSpyのソースの一部を次に示します。上記の手順が実際にどのように実装されているかを見てみてください。コードを単純化するために、エラー処理とUnicodeサポートの部分は省いています。
HANDLE hThread; char szLibPath[_MAX_PATH]; // The name of our "LibSpy.dll" // module (including full path!); void* pLibRemote; // The address (in the remote process) // where szLibPath will be copied to; DWORD hLibModule; // Base address of loaded module (==HMODULE); // initialize szLibPath //... // 1. Allocate memory in the remote process for szLibPath // 2. Write szLibPath to the allocated memory pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath), MEM_COMMIT, PAGE_READWRITE ); ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath, sizeof(szLibPath),NULL ); // Load "LibSpy.dll" into the remote process // (via CreateRemoteThread & LoadLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE )::GetProcAddress( ::GetModuleHandle("Kernel32"), "LoadLibraryA"), pLibRemote, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // Get handle of the loaded module ::GetExitCodeThread( hThread, &hLibModule ); // Clean up ::CloseHandle( hThread ); ::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath),MEM_RELEASE );
実際に割り込ませるコード(SendMessage
)は、DllMain (DLL_PROCESS_ATTACH)
内に書かれているものとします。したがって、この時点で既に実行されています。次は、ターゲットプロセスからDLLをアンロードします。
// Unload "LibSpy.dll" from the target process // (via CreateRemoteThread & FreeLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE )::GetProcAddress( ::GetModuleHandle("Kernel32"), "FreeLibrary"), (void*)hLibModule, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // Clean up ::CloseHandle( hThread );
プロセス間通信
ここまでは、リモートプロセスにDLLを割り込ませる方法についてのみ説明してきました。しかし、たいていの場合は、割り込んだDLLが元のアプリケーションと何らかの方法で通信を行う必要があります(ローカルアプリケーションではなくリモートプロセスにDLLをマッピングしているのだということを思い出してください!)。本稿のサンプルのパスワードスパイについて考えてみましょう。このDLLは、実際にパスワードを含んでいるコントロールへのハンドルを知らなければなりません。当然ながら、この値をコンパイル時にハードコーディングすることはできません。同様に、このDLLはパスワードを取得した後、それを表示するためにアプリケーションに送信しなければなりません。
そのためにはさまざまな方法があり、たとえばファイルマッピング、WM_COPYDATA
、クリップボード、#pragma data_seg
などを使用する方法が考えられます。これらのテクニックについてはMSDNや他のチュートリアルでよく説明されているので、ここでは触れません(プロセス間通信のトピックとして説明されています)。本稿のLibSpyサンプルでは#pragma data_seg
を使用しています。
本稿の最初で紹介しているダウンロードパッケージに、LibSpyの実行可能ファイルとソースファイルが含まれています。