はじめに
シェル上のユーザーの活動履歴をログに残したいと思ったことはないでしょうか。たとえば、アプリケーションの便宜を図るため、クラッシュやブロッキングエラーをリバースエンジニアリングするため、ユーザー活動を監視するためなど、ログを記録する理由はいくつも考えられます。このようなログ機能を実現するうえで鍵になるのは、非常に単純で、しばしば過小評価されているIShellExecuteHook
というCOMインターフェイスです。このインターフェイスを公開するCOMオブジェクトを作成し、適切に登録すれば、Windowsシェル上での処理の実行方法を制御し、場合によっては影響を与えることができるようになります。
必要な環境
このIShellExecuteHook
シェル拡張はWindows 98とWindows 2000でサポートされており、Windows 95とWindows NT 4.0でも、Active Desktop Shell Updateがインストールされている場合のみサポートされます。後者のシステムでは、IE 4.01のセットアップを通じてのみ(しかも必要なオプションをオンにした場合のみ)Active Desktopをインストールできるという点に注意してください。
ShellExecute関数とShellExecuteEx関数
IShellExecuteHook
を実装したCOMオブジェクトは、ShellExecute
およびShellExecuteEx
の呼び出しへのフックを実現するシェル拡張です。この2つのAPI関数には、非常におもしろい機能がいくつかあります。まず、これらの関数では、コマンドライン上のファイル名を受け取り、それに関連付けられた実行可能ファイルの名前を取得することができます。さらに、管理者によって設定されたシステムポリシーに従うことができます。
たとえば、ある権限セットを持つユーザーに対しては、システムポリシーで定義されている特定のグループのアプリケーションを実行させたくないとします。ShellExecute
とShellExecuteEx
を使用すると、新しいプロセスを作成する前にシステムポリシーを調べることができます(CreateProcess
やWinExec
にはこの機能はありません)。この2つの関数の大まかな実行フローは次のとおりです。
- 実行する実行可能ファイルの名前を取得する。この名前は実パラメータとして渡されるか、レジストリから取得される(ファイル名を引数として渡した場合)。
- 取得した実行可能ファイルの名前を実行システムポリシーと照合する。
- 登録されているすべての
IShellExecuteHook
拡張を呼び出す。 - すべてが続行可能である場合は、新しいプロセスを生成して制御を返す。
使用例
ShellExecute
とShellExecuteEx
は、主にWindowsシェルがエクスプローラ上のさまざまな一般的操作を開始するときに使用されます。フォルダ項目をダブルクリックする、フォルダのコンテンツを閲覧する、文書を印刷または編集する、プロパティダイアログボックスを表示する、コンテキストメニューから項目を選択する、といった一般的な操作の合間には、このどちらかの関数が呼び出されています。
また、[スタート]メニューの[ファイル名を指定して実行]ダイアログボックスも、ShellExecuteEx
を通じてプログラムを実行しています。MS-DOSプロンプトから使用できる「start.exe」コマンドラインユーティリティも同様です。つまり、ユーザーがシェルを通じて行う操作はすべてこのShellExecute
フックによって捕捉されるというわけです。さらに、このフックはプログラム的に引き起こされたシェル操作も検出します。
シェルロギングユーティリティの作成
シェルロギングユーティリティを作成するには、まずATLを使用して最小限のCOMオブジェクトを作成します。ここで必要なのは単純なCOMインプロセスオブジェクトなので、生のC++も使用することにします。このATLコンポーネントは、手作業で作成した「IShellExecuteHookImpl.h」ヘッダーファイルからIShellExecutHook
インターフェイスを継承するという形でこのインターフェイスを実装していることに注意してください。
#include <AtlCom.h> #include <ShlObj.h> class ATL_NO_VTABLE IShellExecuteHookImpl : public IShellExecuteHook { public: // IUnknown // STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0; _ATL_DEBUG_ADDREF_RELEASE_IMPL( IShellExecuteHookImpl ) // IShellExecuteHook // STDMETHOD(Execute)(LPSHELLEXECUTEINFO lpsei) { return S_FALSE; } };
リスト1は、ATLオブジェクトのヘッダーファイルの完全なソースコードを示しています。IShellExecuteHook
インターフェイスをパブリックとして定義し、Execute
メソッドをオーバーライドしていることに注目してください。
BEGIN_COM_MAP(CLogger) COM_INTERFACE_ENTRY(ILogger) COM_INTERFACE_ENTRY(IShellExecuteHook) END_COM_MAP()
// ILogger public: STDMETHOD(Execute)(LPSHELLEXECUTEINFO lpsei);
リスト2は、シェルを通じて実行されるすべての操作を検出してログに記録するために必要なコードを示しています。このシェル拡張を適切に登録すると(詳細は後述)、マシン上で動作するソフトウェアがShellExecute
またはShellExecuteEx
を呼び出すたびに、この例のExecute
メソッドが自動的に呼び出されます。Execute
メソッドにはどんな処理でも実行させることができ、操作そのものを停止することも可能です。
Execute
メソッドはSHELLEXECUTEINFO
という構造体を引数として受け取ります。
typedef struct _SHELLEXECUTEINFO{ DWORD cbSize; ULONG fMask; HWND hwnd; LPCTSTR lpVerb; LPCTSTR lpFile; LPCTSTR lpParameters; LPCTSTR lpDirectory; int nCmdShow; HINSTANCE hInstApp; // Optional members LPVOID lpIDList; LPCSTR lpClass; HKEY hkeyClass; DWORD dwHotKey; HANDLE hIcon; HANDLE hProcess; } SHELLEXECUTEINFO, FAR *LPSHELLEXECUTEINFO;
lpOperationメンバとlpFileメンバ
これから実行されようとしている操作はlpVerb
メンバで表されます。また、lpFile
メンバには処理対象のファイルの名前が含まれます。操作はただの文字列で、一部のシェル関連の文書では、これを「verb(動詞:日本語版ではアクション)」と呼ぶこともあります。操作を表す文字列には、
- open
- edit
- explore
- properties
- find
など、コンテキストメニュー項目の名前を使用することができます。
ShellExecute
Execute
メソッドは、シェルが開始しようとしている実行可能ファイルと、これから行おうとしている操作についての情報を受け取ります。ただし、場合によってはlpFile
メンバに実行可能ファイル名が含まれていないことがあります。このような状況になるのは、ShellExecute
が文書ファイル名を受け取ったときです。ユーザーが「.txt」ファイルをダブルクリックしたときは、実際には、そのテキストファイルを実行するようシェルに要求していることになります。そのためWindowsは、そのテキストファイル名を実行可能ファイル名として指定して、ShellExecute
を呼び出します。ShellExecute
は、.txtクラスの文書に関連付けられている「.exe」ファイルの名前を内部的に取得し、その「.exe」ファイルを起動します。この名前を返すのがFindExecutable
というAPI関数です。
Execute内でできること
簡単に言えば、Execute
メソッドは、ユーザーが実行しようとしている実行可能ファイルとその引数、作業ディレクトリ、アクティベーションフラグを受け取ります。ただし、例外であるnCmdShow
を除いては、既定の動作を変更するためにその場で修正できるものはありません。
nCmdShow
は、呼び出し元アプリケーションのメインウィンドウの開き方を定義する引数であり、最小化、最大化、元のサイズなどを表します。これはその場で修正できる唯一のパラメータです。実行可能ファイルの名前やその他のコマンドライン引数に対して加えた変更は、残念ながら無視されます。
そのため、Execute
内でできることはほぼ2つに絞られます。これから起きることを待ち伏せするに留めるか、その操作を自分の責任において完了させるかです。操作のプリプロセスが完了したら、シェルに既定のタスクを続行させるか、それ以上の処理は必要ないことをシェルに伝えることができます。
これはExecute
の戻り値によって決まります。では、1つ目の方法をもう少し詳しく見ていきましょう。
Executeの戻り値
Execute
の戻り値は、「操作が終わったかどうか」という質問に対する答えを表します。答えがS_FALSE
の場合は、そのコマンドに関連付けられている既定の動作をシェルが続行しています。答えがS_OK
の場合は、シェル拡張によって操作が正しく完了されたということであり、シェルは何も行いません。
戻り値がエラーコードの場合、またはその他の未知の値である場合は、エラーメッセージが返されます。では、これをどのように利用したらいいでしょうか。
この機能の利用例として考えられるのは、特定のパラメータが指定されたとき、または特定のユーザーから実行されたときに、特定の実行可能ファイルの実行を阻止することです。これにより、個人的なポリシーマネージャを作成することができます。たとえば次のコード例では、レジストリの設定に関係なく、すべての「.bmp」ファイルの実行を阻止しています。
strlwr((LPSTR)lpsei->lpFile); if (strstr(lpsei->lpFile, _T(".bmp"))) return S_OK;
シェルロギングユーティリティでの処理
シェルロギングユーティリティで行わなければならない処理は、保存の必要があるすべての情報を収集し、ログファイルを更新することだけです。
TCHAR szText[BUFSIZE];
wsprintf(szText, _T("%s: %s at %s\r\n"),
lpsei->lpVerb, lpsei->lpFile, szTime);
上記のコードは、次のような行を含むログファイルを生成します。
open: C:\WINNT\explorer.exe at 5:41:18 PM
ユーザー名やその他のデータを必要に応じて追加すれば、もっと詳しい情報を保存することができます。
別の解決方法
シェルが通常行っている特定のファイルの処理方法や特定の操作の解決方法が気に入らない場合は、独自の方法で実行することができます。たとえば、ある特定のプログラムで、アイコンのプロパティダイアログボックスには一切表示されない(ユーザーには見えない)コマンドラインを使用したいとします。開発者は正しいコマンドラインを使用してプログラムを実行することができ、実行後はそれ以上の処理を行わないようシェルに指示できます。
ただしこの場合は、ShellExecute
またはShellExecuteEx
を使用して実行可能ファイルを実行することはできません。実際、これを行うと、ShellExecute
またはShellExecuteEx
関数に対する新しい呼び出しがShellExecute
フックに対する新しい呼び出しを生成するため、無限ループに入ってしまいます。
実行可能ファイルを実行するには、CreateProcess
またはWinExec
を通じて修正済みのコマンドラインを使用する必要があります。これらの関数は、ShellExecute
フックでは検出されません。プロセスを生成した後は、シェルにS_OK
を返して終了します。
ShellExecuteフックの登録
ShellExecute
フックを機能させるには、一連の適切なレジストリキーが必要です。次のソースコードは、ATLプロジェクトのRGSファイルに追加するATLスクリプトです。
HKLM { SOFTWARE{ Microsoft { Windows { CurrentVersion { Explorer { ShellExecuteHooks { val {CLSID}= s 'Description' }}}}}}}
{CLSID}
文字列の部分を実際のATLオブジェクトのCLSIDに置き換え、コンポーネントの説明を記述します。説明を追加することは必須ではありませんが、わかりやすさのためには追加することをお勧めします。CLSIDは決して理解しやすいものではないので、複数のシェルフックを追加するようになると、どれがどれなのかを識別するのが非常に難しくなります。
複数のShellExecuteフックが作成されている場合
上記のスクリプトでは、「HKEY_LOCAL_MACHINE」の下にツリーを作成し、「Explorer」ノードの下にある「ShellExecuteHooks」の項目に、使用可能なすべてのフックの一覧をまとめます。Windowsのインストール直後は、フックは1つもインストールされていません。しかし、使用しているうちにいくつかのShellExecute
フックが作成されていることがあります。その場合、シェルがどのようなロジックに基づいてフックをロードしているのかは明らかになっていません。
私の推測では、シェル拡張のタイムスタンプに基づいて古いものから順にロードしているのではないかと思っています。レジストリキーのタイムスタンプはRegEnumKeyEx
API関数を使用して取得できます。
しかしここで重要なのは、シェル拡張がS_OKを返したときに、その他の呼び出しをシェルに停止させることです。シェルの動作を仮想コードで表すと次のようになります。
For Each extension In ShellExecuteHooks Load extension Invoke Execute() If return_value Is S_OK Then Exit Next
ご想像のとおり、この方式は、特定の拡張を確実に実行しなければならない場合には問題になります。この問題の回避策を考えてみましょう。
1つの方法は、あなたが作成したシェル拡張をセットアップするときに、既存のシェル拡張をすべて記録したのちに削除することです。その後、あなたのシェル拡張を登録し、他のシェル拡張を再び書き込みます。
こうすれば、シェル側はあなたのシェル拡張を一番古いものとして認識します。
ShellExecuteフックについての注意
ShellExecute
フックはあらゆる状況においてシステムを監視する完璧なツールではないということに注意してください。これは、ShellExecute
とShellExecuteEx
の本体にカスタムコードを挿入するための手段にすぎません。
この2つの関数は主にシェル内で使用されますが、エクスプローラ内で発生し得るすべてのイベントを制御できるとは限りません。
たとえば、コンテキストメニューから使用できるコマンドはすべてShellExecute
を通じて実行されると思っている人もいるかもしれませんが、それはコンテキストメニューの[プロパティ]コマンドに関しては間違いです。[プロパティ]ダイアログボックスをインターセプトしてリダイレクトするためにShellExecute
フックを作成した場合は、それがうまく働かないということに気付くでしょう。ユーザーが任意のフォルダ内の任意のファイルを右クリックして[プロパティ]を選択したときは、SHELLEXECUTEINFO
のlpVerb
フィールドがproperties
に設定されるはずです。
しかし残念ながら、ユーザーがその操作を行ってもシェル拡張はトリガされず、したがってイベントにフックすることもできません。驚いたことに、ちょっとしたC++コードを記述し、次のように「properties
」を動詞として指定してShellExecute
またはShellExecuteEx
を呼び出せば、
ShellExecute(NULL, "properties", "foo.txt", NULL, NULL, SW_SHOW);
フックは正常に呼び出されるのです。これは一体何を意味しているのでしょうか。それは、シェルは[プロパティ]ダイアログボックスを表示するときにShellExecute
を使用していないということです。
IShellExecuteHook
インターフェイスは有用で、いくつかの状況においては大いに役立ちます。しかし、このインターフェイスの使用を上司に提案する前に、それで本当に問題が解決されるのかを確認しておくことを強くお勧めします。というのも、以前私はこれを使って一見不可能だった問題を解決しましたが、それで失敗したことも何度かあるからです。