2006年11月30日
Windowsに土足で乱入?! 〜 フック関数の使い方
今日の多くの OS がそうであるように、Windows にも自作のプログラムコードを特定のプロセスへ介入させることのできる「フック」という仕組みがあります。自分自身のプロセスをフックすることもできますが、他のプロセスをフックすることで通常のアプリケーションプログラミングの枠を超えた様々な興味深い処理の実現が可能となります。
・所定のプロセスに対する Windows メッセージの監視・捕捉
・所定のプロセスでの特定のイベントに呼応する自作コードの注入
・既存のアプリケーションの所作を変更 etc.
「自作のコードをあるプロセスに介入させる」とはどういうことでしょう?
メモリ上のモジュールイメージにアクセスしてマシン語命令を書き換えて・・という怪しげな方法もありそうですが、Windows では DLL を利用することで比較的容易にこの命題を解決できます。
・所定のプロセスに対する Windows メッセージの監視・捕捉
・所定のプロセスでの特定のイベントに呼応する自作コードの注入
・既存のアプリケーションの所作を変更 etc.
「自作のコードをあるプロセスに介入させる」とはどういうことでしょう?
メモリ上のモジュールイメージにアクセスしてマシン語命令を書き換えて・・という怪しげな方法もありそうですが、Windows では DLL を利用することで比較的容易にこの命題を解決できます。
あるプログラムを起動した際のプロセス空間イメージを思い出してみて下さい。
ローダがプログラム実行用に用意したプロセス空間には、実行モジュール本体と実行モジュールの依存する DLL モジュールがマッピングされ、それ以降、DLL 内のコードは実行モジュールのプロセスの一部として透過的に扱われるようになります。
このことを逆に考えると、自作の DLL を所定のプロセスのメモリ空間へ強制的にマッピングしてやれば、そこに含まれるコードは当該プロセスの一部として扱われることになります。この手法は一般に「DLL インジェクション」と呼ばれており、複数の方法で実現することができます。
ここでひとつ疑問が湧いてきます。自作 DLL をあるプロセスへ注入することができたとしても、そこに含まれるコードを当該プロセス内で実行させるにはどうすればよいのでしょう?元の実行モジュールは自作 DLL に関して無意識ですから何もしなければその DLL 内のコードは一行も実行されないはずです。
そのためのひとつの方法が DllMain() 関数呼び出しのメカニズムの利用です。
あるプロセスに DLL が最初にマッピングされる際、Windows はその DLL 内のDllMain() 関数を DLL_PROCESS_ATTACH 通知を伴ってプロセスから呼び出そうとします。つまり、DLL に DllMain() を記述しておき、同通知へのハンドラを用意しておけば自動的に自作コードがプロセス内で実行されるということです。
他に、C++ クラスのインスタンス変数を DLL 内のグローバルスコープで宣言しておくことにより、DLL マッピング時にインスタンス変数のコンストラクタを自動実行させる方法も考えられます。
これらの機構はいずれも自作 DLL が最初にプロセスにマッピングされる際に一度駆動されるのみですが、その機にプロセス内部の所定のアドレス情報を操作することにより特定の Windows API をフックするための準備を整えておけば、当該プロセスに対する持続的な効果を設定することも可能です。
また、Windows の SetWindowsHookEx() API を使えば、DLL インジェクションと同時に、所定のイベントタイプ発生に呼応した自作の「フックプロシージャ」を指定することができます。今回は、手始めにこの API を用いて C で簡単なフックコードを書いてみることにしましょう。
フックコードの試作
手近な題材として「特定のアプリケーションの起動を禁止するプログラム」を作成してみます。ここでは禁止対象とするアプリケーションとして Windows 標準の「メモ帳」を想定します。
まず MSDN サイトで SetWindowsHookEx() の仕様を確認してみましょう。
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_setwindowshookex.asp
この API を使用する上でのポイントは以下の内容であることがわかります。
今回のターゲットはすでに存在するプロセスのスレッドではなく事後に起動するプロセスであるため、システム全体にグローバルフックを適用することになります。
ここまでの情報で、必要なモジュールは以下の exe + DLL の構成になることがわかります。
SetWindowsHookEx() へ指定するパラメータを整理してみます。
idHook には監視するイベントのタイプを指定します。今回はアプリケーションの起動を監視することが目的なので、WH_SHELL を指定します。
lpfn には自作のフックプロシージャのアドレスを指定します。
hmod には本 DLL のモジュールハンドルを指定します。
dwThreadId には監視対象とするスレッドの ID を指定します。今回は 0 を指定することでシステムにグローバルフックをかけます。
WH_SHELL イベントタイプに対応するフックプロシージャの仕様を確認してみましょう。
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_shellproc.asp
nCode の一覧を見ると、HSHELL_WINDOWCREATED に次の説明があります。
フックプロシージャがこの HSHELL_WINDOWCREATED 通知を受けた時点で現在のプロセスがメモ帳であるか否かを判定し、もしそうであればただちに ExitProcess() することで今回の目的を達成できそうです。
フックチェーンを繋ぐための CallNextHookEx()
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_callnexthookex.asp
第一引数 hhk には次の説明があります。
myDLL の中の「SetWindowsHookEx() を呼び出すルーチン」を呼び出すのは前述の通りmyEXE のみであり、CallNextHookEx を呼び出すフックプロシージャはすべてのプロセスから呼び出されます。全プロセスでフックハンドルを共有したいところですが、DLLにおいて通常の方法で宣言したグローバル変数はプロセス間で共有されず各プロセス空間内で独立したインスタンスが作成されることを思い出して下さい。プロセス間でDLL 内の変数を共有する方法については以下の記事を参照して下さい。
<How To Share Data Between Different Mappings of a DLL>
http://support.microsoft.com/kb/125677/en-us
最後に、ここまでの知識をもとに myDLL をコーディングしてみます。
myEXE 側は、適当なトリガーで myDLL のエクスポート関数である StartHook() とEndHook() を任意に呼び出すことができ、プロセスとしてその状態を維持することの可能な内容で実装します。フック中か否かの判定には QueryHookState() 関数を利用するとよいでしょう。
myEXE を起動し、StartHook() 関数を呼び出した状態で他のアプリケーションの起動を試してみましょう。メモ帳のみが起動不可となっていれば成功です。myEXE を終了する前に EndHook() を呼び出してフックを解除することを忘れないで下さい。
なお、サンプルコードでは単純化のために起動禁止とするアプリケーションを単にフルパス名で評価していますが、禁止する exe ファイルのチェックサムをあらかじめMD5 等の信頼性の高い一方向ハッシュ関数で取得の上データ化しておき、ShellProc()内で現プロセスの実行モジュールのチェックサムと照合するといった処理を加えれば実用性が向上するでしょう。また、各エクスポート関数を myEXE 以外から保護する実装を加えることが好ましいでしょう。
次回は特定の Windows API をフックする方法を考えてみます。
ローダがプログラム実行用に用意したプロセス空間には、実行モジュール本体と実行モジュールの依存する DLL モジュールがマッピングされ、それ以降、DLL 内のコードは実行モジュールのプロセスの一部として透過的に扱われるようになります。
このことを逆に考えると、自作の DLL を所定のプロセスのメモリ空間へ強制的にマッピングしてやれば、そこに含まれるコードは当該プロセスの一部として扱われることになります。この手法は一般に「DLL インジェクション」と呼ばれており、複数の方法で実現することができます。
ここでひとつ疑問が湧いてきます。自作 DLL をあるプロセスへ注入することができたとしても、そこに含まれるコードを当該プロセス内で実行させるにはどうすればよいのでしょう?元の実行モジュールは自作 DLL に関して無意識ですから何もしなければその DLL 内のコードは一行も実行されないはずです。
そのためのひとつの方法が DllMain() 関数呼び出しのメカニズムの利用です。
あるプロセスに DLL が最初にマッピングされる際、Windows はその DLL 内のDllMain() 関数を DLL_PROCESS_ATTACH 通知を伴ってプロセスから呼び出そうとします。つまり、DLL に DllMain() を記述しておき、同通知へのハンドラを用意しておけば自動的に自作コードがプロセス内で実行されるということです。
他に、C++ クラスのインスタンス変数を DLL 内のグローバルスコープで宣言しておくことにより、DLL マッピング時にインスタンス変数のコンストラクタを自動実行させる方法も考えられます。
これらの機構はいずれも自作 DLL が最初にプロセスにマッピングされる際に一度駆動されるのみですが、その機にプロセス内部の所定のアドレス情報を操作することにより特定の Windows API をフックするための準備を整えておけば、当該プロセスに対する持続的な効果を設定することも可能です。
また、Windows の SetWindowsHookEx() API を使えば、DLL インジェクションと同時に、所定のイベントタイプ発生に呼応した自作の「フックプロシージャ」を指定することができます。今回は、手始めにこの API を用いて C で簡単なフックコードを書いてみることにしましょう。
フックコードの試作
手近な題材として「特定のアプリケーションの起動を禁止するプログラム」を作成してみます。ここでは禁止対象とするアプリケーションとして Windows 標準の「メモ帳」を想定します。
まず MSDN サイトで SetWindowsHookEx() の仕様を確認してみましょう。
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_setwindowshookex.asp
この API を使用する上でのポイントは以下の内容であることがわかります。
- SetWindowsHookEx() は、指定した DLL の中の所定のフックプロシージャをシステムの管理するフックチェーンへ追加することを目的とするものである
- フックチェーン上に存在する別のフックプロシージャへ通知を受け渡すために、フックプロシージャの中から CallNextHookEx() API を呼び出すべきである
- フックのスコープには特定のスレッドのみを対象とするものと全プロセスを対象とするグローバルフックのふたつがある
- 監視するイベントのタイプは定義済みの定数の中から選択する
- グローバルフックをかけるには、SetWindowsHookEx() 呼び出しを DLL の中から行う必要がある
- SetWindowsHookEx() 後にフックが不要となった時点で UnhookWindowsHookEx()を呼び出さなければならない
今回のターゲットはすでに存在するプロセスのスレッドではなく事後に起動するプロセスであるため、システム全体にグローバルフックを適用することになります。
ここまでの情報で、必要なモジュールは以下の exe + DLL の構成になることがわかります。
[myDLL]
[myEXE] +-------------------------------+
+-----+ | フックプロシージャ本体 |
| | フックを設定 +-------------------------------+
| ------------------> SetWindowsHookEx 呼出し --------+
| | フックを解除 | | |
| ------------------> UnhookWindowsHookEx 呼出し-----+ |
+-----+ | | | |
+-------------------------------+ | |
| |
<システムのフックチェーン> | |
| |
myDLL内 のフックプロシージャが除かれる <-------+ |
|
myDLL内 のフックプロシージャが追加される<---------+
※フックを設定/解除するのは myEXE プロセスのみであることに注意
SetWindowsHookEx() へ指定するパラメータを整理してみます。
HHOOK SetWindowsHookEx(
int idHook, // フックタイプ
HOOKPROC lpfn, // フックプロシージャ
HINSTANCE hMod, // アプリケーションインスタンスのハンドル
DWORD dwThreadId // スレッドの識別子
);
idHook には監視するイベントのタイプを指定します。今回はアプリケーションの起動を監視することが目的なので、WH_SHELL を指定します。
lpfn には自作のフックプロシージャのアドレスを指定します。
hmod には本 DLL のモジュールハンドルを指定します。
dwThreadId には監視対象とするスレッドの ID を指定します。今回は 0 を指定することでシステムにグローバルフックをかけます。
WH_SHELL イベントタイプに対応するフックプロシージャの仕様を確認してみましょう。
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_shellproc.asp
LRESULT CALLBACK ShellProc(
int nCode, // フックコード
WPARAM wParam, // イベント特有の情報
LPARAM lParam // イベント特有の情報
);
nCode の一覧を見ると、HSHELL_WINDOWCREATED に次の説明があります。
1個のトップレベルウィンドウ(親を持たないウィンドウ)が作成されました。
システムが ShellProc 関数を呼び出した時点で、既にこのウィンドウが存在しています。
フックプロシージャがこの HSHELL_WINDOWCREATED 通知を受けた時点で現在のプロセスがメモ帳であるか否かを判定し、もしそうであればただちに ExitProcess() することで今回の目的を達成できそうです。
フックチェーンを繋ぐための CallNextHookEx()
http://msdn.microsoft.com/library/ja/jpipc/html/_win32_callnexthookex.asp
LRESULT CallNextHookEx(
HHOOK hhk, // 現在のフックのハンドル
int nCode, // フックプロシージャに渡すフックコード
WPARAM wParam, // フックプロシージャに渡す値
LPARAM lParam // フックプロシージャに渡す値
);
第一引数 hhk には次の説明があります。
現在のフックのハンドルを指定します。アプリケーションは、SetWindowsHookEx
関数を呼び出した際に取得したハンドルを指定します。
myDLL の中の「SetWindowsHookEx() を呼び出すルーチン」を呼び出すのは前述の通りmyEXE のみであり、CallNextHookEx を呼び出すフックプロシージャはすべてのプロセスから呼び出されます。全プロセスでフックハンドルを共有したいところですが、DLLにおいて通常の方法で宣言したグローバル変数はプロセス間で共有されず各プロセス空間内で独立したインスタンスが作成されることを思い出して下さい。プロセス間でDLL 内の変数を共有する方法については以下の記事を参照して下さい。
<How To Share Data Between Different Mappings of a DLL>
http://support.microsoft.com/kb/125677/en-us
最後に、ここまでの知識をもとに myDLL をコーディングしてみます。
myEXE 側は、適当なトリガーで myDLL のエクスポート関数である StartHook() とEndHook() を任意に呼び出すことができ、プロセスとしてその状態を維持することの可能な内容で実装します。フック中か否かの判定には QueryHookState() 関数を利用するとよいでしょう。
#include <windows.h>
#define CHKPROC_API __declspec(dllexport)
// プロセス間で共有するグローバル変数の定義
#pragma comment(linker, "/section:shared,rws")
#pragma data_seg("shared")
static HHOOK g_hHookProc = NULL; // フックプロシージャハンドル
#pragma data_seg()
// 現プロセス内のグローバル変数
static HINSTANCE g_hDLLMod = NULL;
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hDLLMod = (HINSTANCE)hModule; // 本DLLのモジュールハンドル
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// フックプロシージャ
LRESULT CALLBACK ShellProc(int nCode, WPARAM wParam, LPARAM lParam)
{
char szBuf[MAX_PATH];
if (nCode == HSHELL_WINDOWCREATED) { // トップレベルウィンドウ作成通知
GetModuleFileName(NULL, szBuf, sizeof(szBuf));
// 現プロセスのモジュール名がメモ帳ならこの時点でプロセスを終了
if (stricmp(szBuf, "C:\\WINDOWS\\system32\\notepad.exe") == 0) {
ExitProcess(0);
}
}
// フックチェーン内の次のフックへ繋ぐ
return CallNextHookEx(g_hHookProc, nCode, wParam, lParam);
}
// エクスポート関数:フックの開始
CHKPROC_API BOOL StartHook()
{
if (!g_hHookProc) {
// WH_SHELL タイプのフックプロシージャをグローバルスコープでインストール
g_hHookProc = SetWindowsHookEx(WH_SHELL, (HOOKPROC)ShellProc, g_hDLLMod, 0);
if (!g_hHookProc) {
return FALSE;
}
}
return TRUE;
}
// エクスポート関数:フックの終了
CHKPROC_API void EndHook()
{
if (g_hHookProc) {
UnhookWindowsHookEx(g_hHookProc);
g_hHookProc = NULL;
}
}
// エクスポート関数:フック中か否かを判定
CHKPROC_API BOOL QueryHookState()
{
return (g_hHookProc) ? TRUE : FALSE;
}
myEXE を起動し、StartHook() 関数を呼び出した状態で他のアプリケーションの起動を試してみましょう。メモ帳のみが起動不可となっていれば成功です。myEXE を終了する前に EndHook() を呼び出してフックを解除することを忘れないで下さい。
なお、サンプルコードでは単純化のために起動禁止とするアプリケーションを単にフルパス名で評価していますが、禁止する exe ファイルのチェックサムをあらかじめMD5 等の信頼性の高い一方向ハッシュ関数で取得の上データ化しておき、ShellProc()内で現プロセスの実行モジュールのチェックサムと照合するといった処理を加えれば実用性が向上するでしょう。また、各エクスポート関数を myEXE 以外から保護する実装を加えることが好ましいでしょう。
次回は特定の Windows API をフックする方法を考えてみます。
トラックバックURL
この記事へのコメント
1. Posted by typeR 2006年12月23日 04:17
はじまめして。
Windowsサービスへのフックはどうなりますか?同じやりかたになるのでしょうか。
Windowsサービスへのフックはどうなりますか?同じやりかたになるのでしょうか。
2. Posted by DSAS Staff 2006年12月25日 12:07
サービスコントロールマネージャの管理下にあるサービスプログラムの起動を抑制するには OpenService, StartService 等の API をグローバルスコープでフックする方法が考えられます。
# 次回の記事では API フックを取り上げる予定です
# 次回の記事では API フックを取り上げる予定です
3. Posted by typeR 2006年12月26日 01:25
よろしくお願いします。
尚、外部プロセスからサービスに対して何か処理を一任させる為のフックはどうなるのでしょうか?
尚、外部プロセスからサービスに対して何か処理を一任させる為のフックはどうなるのでしょうか?
4. Posted by DSAS Staff 2006年12月26日 15:54
すみません、ご質問の意図をもうひとつ汲めずにいるのですが、どういった処理の実現を目標とされているのでしょう?