Doctor Scripto のスクリプト ショップ

非同期イベントの監視によるペストウェアの制御

Microsoft Scripting Guys

作業中の Doctor Scripto

Doctor Scripto のスクリプト ショップでは、読者の皆さんから寄せられる実際のシステム管理スクリプトに関する問題を取り上げて、それらを解決するために簡単なスクリプト例を組み合わせてより複雑なスクリプトを作成します。作成するスクリプトは、包括的で強固なものではありませんが、これらのスクリプトを通じて、再利用可能なコード モジュールから効率的なスクリプトを作成する方法、エラーやリターン コードを処理する方法、さまざまなソースから入出力を行う方法、複数のコンピュータに対して実行する方法など、実際のスクリプトの中で実行してみたいテクニックを紹介します。

このコラムやここで紹介するスクリプトが皆さんのお役に立つことを期待します。ご意見、ご要望のほか、皆さんが問題解決のために編み出した方法や、今後取り上げてほしい話題などがありましたら、ぜひお知らせください (英語のみ)。

過去のコラムについては、「Doctor Scripto のスクリプト ショップ」を参照してください。

(メモ : Alain Lissoir、Chris Scoville、Mary Gray、Steve Lee からの役に立つ提案に感謝します。)

トピック

非同期メソッドにより複数のコンピュータ上のプロセスとサービスを監視する
WMI 非同期メソッド
実行中のプロセスのチェック
プロセスの検索と終了
非同期イベント処理を使用したプロセスのトラップ
対象プロセスの終了と監視
サービスの停止と無効化
非同期イベント処理を使用したサービスの監視
不要なプロセスやサービスに対する保護
後記

非同期メソッドにより複数のコンピュータ上のプロセスとサービスを監視する

いまや、ウイルス、ワーム、アドウェア、スパイウェアにとっては良い時代です。これらは、"Rolling Stone" 誌の表紙を飾るほど有名になりました。やや大げさかもしれませんが、実際に "New York Times" 紙で取り上げられ、アドウェアに悩まされたドクターがポップアップ表示された項目をクリックできなくなるという挿絵が掲載されています。また、"Times" 紙には、感染したコンピュータを廃棄してしまったインターネット業界の役員のインタビューも載っています。彼は、コンピュータをクリーンアップするよりも新しいコンピュータを購入した方が費用も時間もかからないと感じているそうです。

断っておきますが、このコラムは Scripting Guys によるウイルス作成教室の広告ではありません。ハッキング行為は、退屈している 16 歳の子供たちには格好良く見えるかもしれませんが、その他のコンピュータ利用者は言うまでもなく、私たちが生計を立てる業界にも多大な被害をもたらします。

まず、ウイルス対策アプリケーションとスパイウェア対策アプリケーションを統合する包括的なセキュリティ戦略ほど優れたものは他にはない、ということを長々と (これは Scripting Guys の得意分野です) 説明しましょう。ただし、この戦略を実践した後でも、スクリプトは、関連する掃討作戦、迅速なカスタマイズや自動化を必要とする他のサポート タスクで依然として有益です。ウイルス対策ベンダが最新のワームに対する署名付きの更新プログラムを用意するまでに何を行いますか。市販のソフトウェアでは対処されない、その他の原因 (恨みを抱く内部関係者など) によって引き起こされる問題についてはどうしますか。また、企業ネットワークの帯域幅を大量に使用する人気のゲームのように、悪意があるのではなく単に不愉快になるだけの問題はどう扱いますか。

このような状況には、不要なソフトウェアが存在する兆候を監視するスクリプトを早急に準備できたら便利ですよね。このような兆候を見つければ、それなりの対処が可能です。

注 : セキュリティの脅威に対処する最適な方法に関する公式のガイダンスについては、以下のリソースを参照してください。

このようなことを考えているのは、私たちだけではありません。南カリフォルニアのコミュニティ カレッジ システムのシステム管理者である Sergio 氏は次のようなアイデアを提案しています。

「ウイルス、ワーム、スパイウェアに関係する悪質なサービスを検出するスクリプトについてどう思いますか。テキスト ファイル内の一覧からサービス名を取得し、このファイルはスクリプトとは別に管理できるようにします。ドメイン全体の複数のコンピュータでこの操作を行います。」

また、インディアナ州出身の Chad 氏は、コンピュータ上の不明なプロセスを探すスクリプトを送ってくれました。

Doctor Scripto はこの問題に懲りすぎて、ペストウェアのパパラッチのようになってしまいました。彼は悪質なコードを追跡し、難解な解決の鍵を解明することが大好きです。でも、解明した後は、そのコードを写真に収めるのではなく、停止してロックアウトしてしまいます (皆さんは、彼は単なる実害のない Scripting Guy だと思ったでしょう)。

Doctor は、この割に合わない仕事をするために、前回のコラムで触れたスクリプト機能をいくつか使用しています。前回のコラムでは、修正プログラム、アップグレード、インストール プログラム、その他のユーティリティを扱うときに、プロセスをいくつか実行して、それらの実行期間を追跡する方法について説明しました。今回はプロセスの作成については説明しないで、多数のコンピュータ上で作業が必要な場合に特にうまく動作するさまざまなイベント監視方法について詳しく説明します。このような方法を、非同期イベント監視と呼びます。

非同期イベント監視を使用して、特定の不要なプロセスやサービスをトラップし、それらを停止または無効にします。これらのスクリプトで示している原理を調整して、歓迎しないコードによって生成される可能性のある他の種類の兆候を検出して処理することもできます。

WMI 非同期メソッド

Doctor Scripto の前回のコラムでは、WMI スクリプト API の SWbemServices オブジェクトの ExecNotificationQuery (英語) メソッドを呼び出し、スクリプトで実行したプロセスを追跡しました。

ところが、複数のコンピュータ上でイベントを監視したところ、このメソッドでは問題が発生しました。WMI メソッドは、同期、半同期、または非同期に実行できます。ExecNotificationQuery は、同期または半同期 (VBScript の既定値) のイベント監視メソッドです。同期メソッドや半同期メソッドを実行することは、この場合、1 台目のコンピュータの ExecNotificationQuery から返された SWbemEventSource (英語) オブジェクトが NextEvent メソッド を呼び出すためにそのコンピュータでイベントが発生するのを待機して応答を停止し、スクリプトが他のコンピュータのイベント監視に移行できないことを意味します。

今回は、一覧内のコンピュータごとに新しいスクリプトを作成し、監視するプロセスのコンピュータ名とプロセス ID をそのスクリプトに渡すことでこの問題を解決しました。これは間違った解決策ではありませんでしたが、複数のスクリプトを使用する必要があるので、複雑怪奇なコンピュータという雰囲気を漂わせます。

Doctor Scripto は、勇敢にも WMI の内部を詳しく調べ、使い道は少なくても、洗練された方法で、複数のコンピュータのイベントを監視する方法を考案しました。いつもお話しているように、Scripting Guys にとっては洗練さがすべてではありませんが、洗練さだけはあります。

非同期メソッドは、テレビの自然を扱った番組に似ているところがあります。このような番組では、カメラマンが荒野や森林に分け入り、仕掛け付きの隠しカメラを設置して、その土地の野生生物の往来を記録します。カメラマンは、シャッターを切るためにその場にいる必要はありません。仕掛けがカメラマンの代わりにシャッターを切り、カメラが画像を記録します。ただし、このコラムでは、追跡対象がヒヒやオリックスではなく、問題のあるプロセスやサービスになります。

SWbemServices (英語) オブジェクトは、WMI スクリプト API の主力オブジェクトの 1 つで、ほとんどのサンプル スクリプトで使用しています。このオブジェクトには、同期メソッドや半同期的メソッドと兄弟関係にある、いくつかの非同期メソッドがあります。たとえば、ExecQuery には ExecQueryAsync、InstancesOf には InstancesOfAsync があります。さらに、Doctor にとって嬉しいことに、ExecNotificationQuery には ExecNotificationQueryAsync(英語) があります。

同期メソッドや半同期メソッドがより直接的な方法 (操作を実行し、何が起こるかを調べるために一時停止して、その後結果を返す方法) を取るのに対して、非同期メソッドはクエリを行うとすぐに復帰します。ただし、非同期メソッドは、"シンク" と呼ばれるものを残します。これは、非同期メソッドの結果を受け取るのを待機する特殊なオブジェクトです。WMI スクリプト API には、非同期メソッドに対してこの機能を実行する、SWbemSink (英語) という特種なオブジェクトがあります。

メソッドが調査対象の進行中の活動をレポートするために残すシンクは、遠隔カメラやセンサと考えてください。

ExecNotificationQueryAsync メソッドの場合、調査する結果はイベントです。そのため、WMI ではこのメソッドを呼び出すスクリプトをイベント コンシューマと呼びます。ここでは、SWbemSink オブジェクトは、クエリで指定した種類のイベントが発生するのを対象のコンピュータで待機する、"イベント シンク" として機能します。このようなイベントが発生すると、SWbemSink の OnObjectReady イベントが発生するので、スクリプトではこれをキャッチして処理します (非同期イベント監視の詳細については、「WMI SDK」(英語) を参照してください)。

ExecNotificationQueryAsync の実行
OnObjectReady を待機するシンク

私の詩的センスでは彼らの発言は耐えられないものです。

実際には、1 つの SWbemSink オブジェクトで 3 台のリモート コンピュータを監視でき、スクリプトを実行しているローカル コンピュータにこのオブジェクトのインスタンスが作成されます。

複数のコンピュータでイベントを監視する際に ExecNotificationQueryAsync メソッドを使用する大きなメリットは、コンピュータごとに個別にスクリプトを開始するが必要なく、同じスクリプト内でイベント シンクを使用してこれらのコンピュータを監視できることです。

前々回のコラム「午前 2 時、プロセスの所在を把握していますか」では、イベント クエリの基礎について説明しました。ちなみに、WMI イベントを初めてお使いになる場合は、Scripting Guys の Web キャスト「An Ounce of Prevention: An Introduction to WMI Events」(英語) を確認してください。

このコラムの後半では、非同期イベント監視のこのようなメリットを利用することになります。ただし、まず、Dr. Scripto のすばらしい監視に必要な、簡単で使い慣れたスクリプト タスクを復習しましょう。

実行中のプロセスのチェック

ある望ましくないプロセスがコンピュータで実行されていることを懸念する場合は、最も基本的なスクリプトの 1 つを改良して、まず、このようなプロセスの有無をチェックできます。Scripting Guys Web キャストやワークショップに参加したり、「Windows 2000 Scripting Guide」をお読みになっていれば、おそらく、コンピュータで実行されているプロセスの一覧を表示する簡単なスクリプトの 1 つを見たことがあるでしょう。

このスクリプトを一歩進めて、このような実行中のプロセスをプロセス名の一覧と照合し、一致するプロセスを表示します。望ましくないプロセスの代わりに電卓、メモ帳、およびフリーセルを使用します。このスクリプトをテストするには、これらの各アプリケーションのウィンドウを複数開きます。

リスト 1 : プロセスを検索する

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")

For Each objProcess in colProcesses
  For Each strTargetProc In arrTargetProcs
    If LCase(objProcess.Name) = LCase(strTargetProc) Then
      WScript.Echo objProcess.Name
    End If
  Next
Next

チェックするプロセスの一覧を VBScript の配列に格納します。このような配列は、項目数が少ない場合にうまく機能します。WMI に実行中のプロセスのコレクションを照会してから、入れ子になった For Each ループを使用して、一覧内のプロセスをコンピュータで実行中のプロセスと比較します。

最初の For Each ステートメントは、WMI から返されるプロセスのコレクションをループします。内側の For Each ステートメントでは、このようなプロセスごとに配列内のプロセスをループし、各プロセスを現在実行中のプロセスと照合します。誤った比較が行われないように、VBScript の組み込み関数 LCase を使用してプロセス名をすべて小文字にします。これは、1 つの名前にさまざまな大文字小文字の組み合わせが使われるためです。一致しているものを見つけると、そのプロセスの名前を表示します。多数の実行可能ファイルに対して、同じ名前の複数のプロセスが実行されている可能性があることに注意してください。

さて、コンピュータ上で無駄に実行しておきたくないプロセスを特定できました。どのように処理しましょうか。

プロセスの検索と終了

便利なことに、WMI クラス Win32_Process には、Terminate (英語) というメソッドが用意されています。このメソッドは、ほぼ予想どおりの動作を行います。最初に、終了するプロセス オブジェクトへの参照を取得してから、そのオブジェクトで Terminate メソッドを呼び出します。ある意味、そのプロセスが自動的に消滅することを求めているとも言えます。

objProcess.Terminate

この婉曲的な表現は、同じことを実行するコマンド ライン ツール、Kill.exe や Taskkill.exe (Windows 2000 以降) の殺伐とした命名とは対照的です。

後者の命名であれば、次のスクリプトは一種のアクション映画のように、ヒーローである Dr. Scripto が捜索と撲滅のために悪意のあるプロセス群に踏み込んでいく様子が目に浮かびます。あるいは、あまり敵意のない表現をお好みの場合は、雑草のごときプロセスを堆肥の山に投げ込み、すばらしい炭素サイクルによって根絶させ、平和な庭を築き上げるというのはどうでしょう。まったく問題はありません。皆さん優しいですね。Scripting Guys は喜んでもらえるように努力します。

リスト 1 と同様に、スクリプトを実行する前に、対象のプロセスのインスタンスを必要な数だけ開き、プロセスの実行を常に監視します。

リスト 2 : プロセスを終了する

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")

Wscript.Echo "Checking for target processes ..."

For Each objProcess in colProcesses
  For Each strTargetProc In arrTargetProcs
    If LCase(objProcess.Name) = LCase(strTargetProc) Then
      WScript.Echo VbCrLf & "Process Name: " & objProcess.Name
      WScript.Echo "  Time: " & Now
      intReturn = objProcess.Terminate
      If intReturn = 0 Then
        WScript.Echo "  Terminated"
      Else
        WScript.Echo "  Unable to terminate"
      End If
    End If
  Next
Next

リスト 2 は、実行中のプロセスを配列の一覧にある名前と比較するところまではリスト 1 とよく似ています。ただし、一致するプロセスの名前をエコー出力するのに加えて、一致したプロセスで Terminate メソッドも呼び出しています。その後、Terminate から返された値をチェックします。ほぼすべての WMI メソッドと同様に、値が 0 (プロセスが正常に終了したことを示します) の場合、その影響に関するメッセージを表示します。その他の値は、何らかの問題が発生し、メソッドが正常に実行されなかったことを示します (値の一覧は WMI SDK トピックに記載されています)。

非同期イベント処理を使用したプロセスのトラップ

考えてみると、2 番目のスクリプトはプロセスを停止しているだけで、プロセスを実行した実行可能ファイルを削除していません。フリーセルの愛好家はすぐにゲームを再開してしまうのではないでしょうか。また、ウイルスが不正なメカニズムを使ってそのプロセス自体を再起動したらどうなるのでしょう。

実行可能ファイルを削除することはできますが、それがゲームであれば、削除まではやり過ぎになってしまう可能性があります。状況によっては、そのファイルを削除する必要がない場合もあります。スクリプトを作成してこのような状況に対処できると考えられる操作は数多くあります。Doctor Scripto はこのコラムでこのような操作すべてを取り上げたいと思っていますが、私たちはそれが彼の最初の作品になることは望んでいません。そこで、1 つの方法として、不要なプロセスが再開されないようにすることに注目しましょう。

アクション映画の観点では、"ダブルボーダー (Extreme Prejudice)" のようにこのようなプロセスを終了しましょう。Doctor Scripto は、庭の雑草に匹敵する例えを考え出そうとしましたが、思い浮かびませんでした。

先に説明した非同期監視が出てくる箇所は次のとおりです。リスト 3 では、リスト 2 でシャットダウンしたのと同じプロセスを監視し、これらのプロセスが再開されたら、再び終了します。このコードは、オペレーティング システム用の統合ペスト管理と考えてください。

テスト目的にこのスクリプトを実行するには、"Waiting for target processes ..." というメッセージが表示されたら対象のアプリケーションを開きます。スクリプトは、ほぼ瞬時にそのアプリケーションを終了します。本当のところ、皆さんはこのスクリプトとほど面白かったスクリプトはありましたか。

リスト 3 : プロセスを非同期に監視して終了する

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"

Wscript.Echo "Waiting for target processes ..."

Do
   WScript.Sleep 1000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.TargetInstance.Name) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.TargetInstance.Name
    Wscript.Echo "  Time: " & Now
'Terminate process.
    intReturn = objLatestEvent.TargetInstance.Terminate
    If intReturn = 0 Then
      Wscript.Echo "  Terminated"
    Else
      Wscript.Echo "  Unable to terminate"
    End If
  End If
Next

End Sub

ExecNotificationQueryAsync に渡す WQL クエリでは、WITHIN 1 を使用してポーリング間隔を定義していることに注意してください。つまり、クエリは 1 秒ごとにポーリングします。簡単なデモではうまく機能します。ところが、多数のコンピュータや大量のデータを返すクエリでこのようなスクリプトを実行するには、ポーリング間隔にもっと大きな数値を使って実験する必要があります。WITHIN 10 にすると、使用する処理帯域幅が少なくなり、リモート コンピュータで実行する場合は、作成されるネットワーク アクティビティが少なくなります。もちろん、弱点としては、不要なプロセスが終了するまでの実行時間が長くなることが挙げられます。特定の環境に最適な設定は、その状況を考慮して計画およびテストする必要があります。

非同期メソッドでは、スクリプトでシンクを作成する必要があります。これについては、既に説明しました。スクリプトでシンクを作成するには、2 つのパラメータを渡して、Windows スクリプト ホストのメソッド CreateObject を呼び出します。渡すパラメータは、プログラム ID WbemScripting.SWbemSink と識別子 (この場合は SINK_) で、この識別子を OnObjectReady などの SWbemSink メソッドの先頭に追加します。このようなメソッドはスクリプトの後半で呼び出します。

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

作成したシンクには、イベントが関連付けられます。ここで対象となる主なイベントの 1 つが OnObjectReady です。非同期呼び出しで指定されたオブジェクトがシンクで使用できるようになると、つまりこの場合はプロセスの作成が行われるたびに、OnObjectReady が発生します。このようなイベント オブジェクトを処理するには、CreateObject 呼び出しで指定したプレフィックス (SINK_) で始まり、イベントの名前で終了するサブルーチンを記述する必要があります。したがって、イベントをトラップするサブルーチンは SINK_OnObjectReady になります。

WMI に接続して ExecNotificationQueryAsync メソッドを呼び出す際に、次の 2 つのパラメータを渡します。

  • 先ほど作成したシンク オブジェクトの名前。この場合は SINK です (ここでは独創性はいりません)。

  • 実行する WQL イベント クエリ。この場合は、"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'" です。

ここでは、1 秒ごとにチェックして、プロセスが作成されたイベントについて通知するように WMI に求めています。その後、スクリプトはイベントが発生するまで待機する永久ループに進みます。ループを終了するには、Ctrl キーを押しながら C キーを押します。

クエリの条件に一致するイベントが発生すると、SINK_OnObjectReady が出力パラメータを返します。このパラメータを使用して、新しいイベントを表すオブジェクト objLatestEvent を呼び出します (このスクリプトでは、objAsyncContext という他のパラメータは使用しません)。前 2 回のコラムで説明した同期クエリと半同期クエリと同様に、objLatestEvent の TargetInstance プロパティにより、スクリプトはイベントが発生したオブジェクト (この場合は Win32_Process オブジェクト) のインスタンスのすべてのプロパティにアクセスできます。その結果、objLatestEvent.TargetInstance.Name を使用して、プロセスの名前を検索できます。さらに嬉しいことに、TargetInstance を使用すると、そのインスタンスのメソッドを呼び出すこともできます。

ここで、イベントが発生したプロセスの名前を使用して対象プロセスの一覧をループし、プロセスが一致するかどうかを確認します。一致する場合は、インスタンス オブジェクトの Terminate メソッドを呼び出します。

intReturn = objLatestEvent.TargetInstance.Terminate

プロセスは開始および実行される前に、再び終了されます。これはあまり公平とは言えません。

対象プロセスの終了と監視

ここで、リスト 2 とリスト 3 のスクリプトをまとめてみましょう。このスクリプトでは一覧で指定したプロセスを終了してから、これらのプロセスが再開されないように非同期に監視します。また、複数のコンピュータに対してそのスクリプトを実行する必要もあります。

スクリプトをテストするには、arrComputers のコンピュータ名を、読者に管理者特権のあるネットワーク上でアクセスできるコンピュータ名に置き換えます。対象プロセスのインスタンスをいくつか開きます。スクリプトでそれらのインスタンスが閉じられたら、再度開きます。

リスト 4 : 対象プロセスを終了してから新しいプロセスを監視する

On Error Resume Next

arrComputers = Array("sea-wks-1", " sea-wks-2", " sea-srv-1")
g_arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

For Each strComputer In arrComputers
  WScript.Echo vbCrLf & "Host: " & strComputer
  intKP = KillProcesses(strComputer)
  If intKP = 0 Then
    TrapProcesses strComputer
  Else
    WScript.Echo vbCrLf & "  Unable to monitor target processes."
  End If

Next

Wscript.Echo VbCrLf & _
 "     -----------------------------------------------------------------" & _
 VbCrLf & VbCrLf & "In monitoring mode ..."
 
Do
   WScript.Sleep 1000
Loop

'******************************************************************************

Function KillProcesses(strHost)
'Terminate specified processes on specified machine.

On Error Resume Next

strQuery = "SELECT * FROM Win32_Process"
intTPFound = 0
intTPKilled = 0

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strHost & "\root\cimv2")
If Err = 0 Then
  WScript.Echo vbCrLf & "  Searching for target processes."
  Set colProcesses = objWMIService.ExecQuery(strQuery)
  For Each objProcess in colProcesses
    For Each strTargetProc In g_arrTargetProcs
      If LCase(objProcess.Name) = LCase(strTargetProc) Then
        intTPFound = intTPFound + 1
        WScript.Echo "  " & objProcess.Name
        intReturn = objProcess.Terminate
        If intReturn = 0 Then
          WScript.Echo "    Terminated"
          intTPKilled = intTPKilled + 1
        Else
          WScript.Echo "    Unable to terminate"
        End If
      End If
    Next
  Next

  WScript.Echo "  Target processes found: " & intTPFound
  If intTPFound <> 0 Then
    WScript.Echo "  Target processes terminated: " & intTPKilled
  End If
  intTPUndead = intTPFound - intTPKilled
  If intDiff <> 0 Then
    WScript.Echo "  ALERT: Target processes not terminated: " & intTPUndead
  End If
  KillProcesses = 0
Else
  HandleError(strHost)
  KillProcesses = 1
End If

End Function

'******************************************************************************

Sub TrapProcesses(strHost)

On Error Resume Next

strAsyncQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"

'Connect to WMI.
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strHost & "\root\cimv2")
If Err = 0 Then
'Trap asynchronous events.
  objWMIService.ExecNotificationQueryAsync SINK, strAsyncQuery
  If Err = 0 Then
    WScript.Echo vbCrLf & "  Monitoring target processes."
  Else
    HandleError(strHost)
    WScript.Echo "  Unable to monitor target processes."
  End If
Else
  HandleError(strHost)
  WScript.Echo "  Unable to monitor target processes."
End If

End Sub

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

For Each strTargetProc In g_arrTargetProcs
  If LCase(objLatestEvent.TargetInstance.Name) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Target process on: " & _
     objLatestEvent.TargetInstance.CSName
    Wscript.Echo "  Name: " & objLatestEvent.TargetInstance.Name
    Wscript.Echo "  Time: " & Now
'Terminate process.
    intReturn = objLatestEvent.TargetInstance.Terminate
    If intReturn = 0 Then
      Wscript.Echo "  Terminated process."
    Else
      Wscript.Echo "  Unable to terminate process. Return code: " & intReturn
    End If
  End If
Next

End Sub

'******************************************************************************

Sub HandleError(strHost)
'Handle errors.

strError = VbCrLf & "  ERROR on " & strHost & VbCrLf & _
 "  Number: " & Err.Number & VbCrLf & _
 "  Description: " & Err.Description & VbCrLf & _
 "  Source: " & Err.Source
WScript.Echo strError
Err.Clear

End Sub

このスクリプトでは、実行中のプロセスを最初に終了するコードを KillProcesses 関数に追加し、プロセス イベントを監視するコードを TrapProcesses サブルーチンに追加しています。また、予想していたと思いますが、エラーを処理するコードを HandleError サブルーチンに分離しているので、複数の異なる場所でエラー処理コードを繰り返す必要がなくなります。ご覧のように、プロシージャには、実行内容がわかるように動詞と名詞を組み合わせる形式で、簡単明瞭な名前を付けようと試みています。ご想像どおり、あらゆる段階で、私たちは Doctor Scripto と戦わなくてはなりません。彼は、"OffTheProcess"、"FunkyFunkyAsync"、"DoTheErrNow" のような名前を好むからです。

スクリプトの先頭にあるメイン ロジック セクションで、イベント シンクを作成します。前のスクリプトと同様に、シンクの OnObjectReady イベントを処理するサブルーチンも (スクリプトの下の方に) 作成します。

対象プロセスの配列名を "g_arrTargetProcs" に変更したことに注意してください。"g_" というプレフィックスは、グローバル変数を示す記法です。グローバル変数はスクリプト本体だけでなく、すべてのサブルーチンや関数で使用できます。このためグローバル変数を使用すると、プロセス名を各プロシージャに個別に渡す際にトラブルが起きなくなります。

その後、コンピュータの一覧をループします。各コンピュータでは、まず KillProcesses を呼び出します。これにより、リモート コンピュータ上の WMI に接続できない場合は 1 が返されます。その場合には、スクリプトはイベント監視を試みないで、次のコンピュータに移ります。

KillProcesses は、少なくともコンピュータに接続できた場合は、対象プロセスの有無をチェックします。対象プロセスが見つかれば、そのプロセスの終了を試みます。見つかったプロセス、終了されたプロセス、および終了されていないプロセスを追跡し、これらの数をメッセージに表示します。

KillProcesses がコンピュータに接続している限り、一部のプロセスを終了できなかったとしても、メイン ロジックを先へ進み、TrapProcesses サブルーチンを呼び出します。これにより、そのコンピュータで非同期イベントのクエリが実行されます。最初のパスで一部のプロセスを終了できなかった場合でも、新しいプロセスを監視するために先に進みます。当然ですが、KillProcesses で対象プロセスが見つからなかった場合、スクリプトはプロセスの再開に備えて、これらのプロセスの監視を続行します。

見つかった場合、非同期イベントのクエリは、そのコンピュータで監視しているイベントを、スクリプトの先頭で作成した中心となるイベント シンクに接続します。すべてのコンピュータで監視しているイベントは同じシンクにフィードバックされますが、シンクは Win32_Process の CSName (コンピュータ システムの名前) プロパティをチェックすることで、イベントが発生したコンピュータを識別できます。

前のスクリプトと同様に、TrapProcesses は、対象となるプロセスを見つけると、すぐに Terminate を呼び出し、プロセスを停止しようとします。停止できた場合は、FunkyFunkyAsync を実行します。

サービスの停止と無効化

さて、プロセスを扱う簡単な作業スクリプトを説明してきましたが、まだサービスについては一切説明していません。多くのアプリケーションは、プロセスではなくサービスの形で実行されます。プロセスを扱ったように、不要なサービスも扱う方法はあるのでしょうか。

サービスに対する解決策は、プロセスのスクリプトとよく似ています。目標は、不要なサービスを無効にして再開しないようにすることですが、ここで厄介な問題が発生します。サービスは実行中に無効にしても、停止されません。無効後に変更できるのは、スタートアップの種類だけです。したがって、スクリプトでは、サービスを再開できないように、サービスの停止と無効化の両方を実行する必要があります。

次のスクリプトを試す場合は、スクリプトを実行する前に、テストする各サービスの状態とスタートアップの種類をメモし、実行後に元の状態に戻すようにしてください。この例では、テスト対象としてオペレーティング システムのサービス Alerter、Smart Card、および Wireless Zero Configuration を使用しています。必要に応じて、他のサービスと置き換えることができます。arrTargetSvcs では、Services.msc で使用されている用語に従って、表示名ではなくサービス名を使用する必要があることに注意してください。

リスト 5 : サービスを停止して無効にする

On Error Resume Next

strComputer = "."
arrTargetSvcs = Array("Alerter", "SCardSvr", "WZCSVC")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colServices = objWMIService.ExecQuery("SELECT * FROM Win32_Service")

Wscript.Echo "Checking for target services ..."

For Each objService in colServices
  For Each strTargetSvc In arrTargetSvcs
    If LCase(objService.Name) = LCase(strTargetSvc) Then
      WScript.Echo VbCrLf & "Service Name: " & objService.Name
      WScript.Echo "  Status: " & objService.State
      Wscript.Echo "  Startup Type: " & objService.StartMode
      WScript.Echo "  Time: " & Now
      If objService.State = "Stopped" Then
        WScript.Echo "  Already stopped"
      Else
        intStop = objService.StopService
        If intStop = 0 Then
          WScript.Echo "  Stopped service"
        Else
          WScript.Echo "  Unable to stop service"
        End If
      End If
      If objService.StartMode = "Disabled" Then
        WScript.Echo "  Already disabled"
      Else
        intDisable = objService.ChangeStartMode("Disabled")
        If intDisable = 0 Then
          WScript.Echo "  Disabled service"
        Else
          WScript.Echo "  Unable to disable service"
        End If
      End If
    End If
  Next
Next

ここでの ExecQuery のクエリは、リスト 1 で使用したクエリとよく似ています。ただし、Win32_Process ではなく Win32_Service クラスを使用しています。このメソッドは、コンピュータにインストールされているすべてのサービスのコレクションを返します。

サービスの状態とスタートアップの種類 (サービス スナップインで使用される名前) は無関係です。サービスは、実行中に無効にされても実行を続けます。逆に、サービスを停止しても、無効にはできません。そのため、サービスの停止と無効化の両方を実行することが必要です。

このような 2 つの作業を実行するには、最初に、スクリプトでサービスが実行中かどうかをチェックします。Win32_Service は State プロパティを使用して、スナップインでの [状態] を表します。したがって、スクリプトでは State プロパティをチェックします。この値が "Stopped" でなければ、StopService メソッドを呼び出します。

次に、StartMode プロパティをチェックします。スナップインでは、このプロパティが [スタートアップの種類] と表示されます。この値が "Disabled" ではなければ、唯一のパラメータとして "Disabled" を渡して、ChangeStartMode メソッドを呼び出します。

スクリプトの実行が終了すると、対象のサービスがすべて停止され無効になります。

非同期イベント処理を使用したサービスの監視

プロセスと同様に、悪意のあるサービスが停止状態や無効状態のままでいるとは限りません。そのため、次のスクリプトでは非同期イベントのクエリを使用して、一覧にあるサービスが再開されないように監視する方法を示しています。

このスクリプトはいつまでも実行し続けます。スクリプトを終了するには、Ctrl キーを押しながら C キーを押してください。

リスト 6 : サービスを非同期で監視、停止、および無効にする

On Error Resume Next

strComputer = "."
arrTargetSvcs = Array("Alerter","SCardSvr","WZCSVC")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM __InstanceModificationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Service'"

Wscript.Echo "Waiting for target services ..."

Do
   WScript.Sleep 1000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

For Each strTargetSvc In arrTargetSvcs
  If LCase(objLatestEvent.TargetInstance.Name) = LCase(strTargetSvc) Then
    Wscript.Echo VbCrLf & "Service Name: " & objLatestEvent.TargetInstance.Name
    Wscript.Echo "  Status: " & objLatestEvent.TargetInstance.State
    Wscript.Echo "  Startup Type: " & objLatestEvent.TargetInstance.StartMode
    Wscript.Echo "  Time: " & Now
'Stop service.
    If objLatestEvent.TargetInstance.State = "Stopped" Then
      WScript.Echo "  Already stopped"
    Else
      intStop = objLatestEvent.TargetInstance.StopService
      If intStop = 0 Then
        WScript.Echo "  Stopped service"
      Else
        WScript.Echo "  Unable to stop service"
      End If
    End If
    If objLatestEvent.TargetInstance.StartMode = "Disabled" Then
      WScript.Echo "  Already disabled"
    Else
      intDisable = objLatestEvent.TargetInstance.ChangeStartMode("Disabled")
      If intDisable = 0 Then
        WScript.Echo "  Disabled service"
      Else
        WScript.Echo "  Unable to disable service"
      End If
    End If
  End If
Next

End Sub

このスクリプトも、リスト 3 のプロセスの監視と終了を実行するスクリプトによく似ています。異なるのは、リスト 5 ではサービスを停止して無効にしている点です。

この場合 ExecNotificationQueryAsync に渡されるクエリは、TargetInstance が Win32_Process ではなく Win32_Service の場合にフィルタします。また、リスト 3 のスクリプトで選択した __InstanceCreationEvent ではなく、__InstanceModificationEvent のインスタンスを選択しています。これは、対象となるサービスが、すべて既にインストールされているためです。つまり、起動モードやスタートアップの変更のイベントは、インスタンスの作成ではなく変更になります。

SINK_OnObjectReady サブルーチンは、対象イベントがトラップされると、サービスの停止と無効化の両方を実行しなければなりません。これらの作業を実行するには、Win32_Service の 2 つのメソッド (StopService と ChangeStartMode) を呼び出し、後者にはパラメータとして "Disabled" を渡します。

不要なプロセスやサービスに対する保護

各作業を実行するのに必要な一連のコードを確認したところで、Doctor Script は溶接トーチを作動して、すべてのコードを 1 つの包括的なスクリプトにまとめることにしました。このスクリプトは、今回のコラムでここまでに紹介したすべてのスクリプトをあわせたものです。

最終的なスクリプトでは、次の操作を実行します。

  • テキスト ファイルからプロセスの一覧を取得します。

  • テキスト ファイルからサービスの一覧を取得します。

  • テキスト ファイルからコンピュータの一覧を取得します。

  • 各コンピュータでは、次の操作を実行します。

    • 一覧のプロセスの有無をチェックします。

    • プロセスが見つかったら、そのプロセスを終了します。

    • 一覧のサービスの有無をチェックします。

    • サービスが見つかったら、そのサービスを停止および無効にします。

    • 一覧内のすべてのプロセスの __InstanceCreationEvent を監視するために非同期イベント シンクを作成します。

    • これらのプロセスのいずれかが再開される場合は終了します。

    • 一覧にあるすべてのサービスの __InstanceModificationEvent を監視するために非同期イベント シンクを作成します。

    • これらのサービスのいずれかが再開される場合は停止および無効にします。

  • 結果をテキスト ログ ファイルに記録し、コマンドシェル ウィンドウに表示します。

スクリプトのロジック - 最終的なスクリプト

私たちのコラムの常として、このスクリプトは皆さんが使用する際に拡張するためのテンプレートです。ご使用の環境で動作させるにはカスタマイズする必要があります。また、改善したり追加する必要も出てくるでしょう。このスクリプトは、Windows XP および Windows Server 2003 を実行しているコンピュータの小規模なグループでテストしましたが、多数のコンピュータ、または異なる種類のコンピュータが混在する環境に対して徹底的なテストを行っていません。

入力用のテキスト ファイルの例を次に示します。すべてのファイルはスクリプトと同じディレクトリに置く必要があります。ファイル名とパスは、スクリプトの先頭にある変更ブロック (Change Block) で変更できます。

このスクリプト例では、次のファイル名は complist.txt です。コンピュータ名を、ご使用のネットワークからアクセスできるコンピュータまたは管理者特権を持つコンピュータの名前に変更します。

リスト 7a : コンピュータの一覧を記述したテキスト ファイル

sea-wks-1
sea-wks-2
sea-srv-1

このスクリプト例では、次のファイル名は proclist.txt です。監視を希望するプロセスのプロセス名に置き換えてください。

リスト 7b : プロセスの一覧を記述したテキスト ファイル

calc.exe
notepad.exe
freecell.exe

このスクリプト例では、次のファイル名は svclist.txt です。監視を希望するサービスのサービス名 (表示名ではありません) に置き換えてください。

リスト 7c : サービスの一覧を記述したテキスト ファイル

Alerter
SCardSvr
WZCSVC

このスクリプトをテストするには、対象プロセスのインスタンスを複数開いて、対象サービスの状態をチェックして、停止されておらず、無効にもなっていないことを確認します。スクリプトにより、プロセスが終了し、サービスが停止および無効になったら、プロセスを再実行し、サービスのスタートアップの種類を手動または自動に変更します (サービスが無効になっているときは開始できません)。スクリプトはプロセスを停止し、サービスのスタートアップの種類を無効に戻すはずです。

このスクリプトはいつまでも実行し続けます。cmd.exe ウィンドウで実行されているスクリプトや実行可能ファイルと同様に、Ctrl キーを押しながら C キーを押してウィンドウを終了します。

リスト 7 : 対象プロセスを終了し、対象サービスを停止および無効にして、再開を防ぐためにすべて監視する

On Error Resume Next

'******************************************************************************
'Change block - change values to fit local environment.
strProcList = "proclist.txt"
strSvcList = "svclist.txt"
strCompList = "complist.txt"
g_strOutputFile = "c:\scripts\cleanup-log.txt"
'******************************************************************************

g_arrTargetProcs = ReadTextFile(strProcList)
g_arrTargetSvcs = ReadTextFile(strSvcList)
arrComputers = ReadTextFile(strCompList)

Set SINK1 = WScript.CreateObject("WbemScripting.SWbemSink","SINK1_")
Set SINK2 = WScript.CreateObject("WbemScripting.SWbemSink","SINK2_")

For Each strComputer In arrComputers
  strMessage1 = vbCrLf & "Host: " & strComputer
  WScript.Echo strMessage1
  WriteTextFile g_strOutputFile, strMessage1
  Set g_objWMIService = GetObject("winmgmts:" _
   & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  If Err = 0 Then
    intKP = KillProcesses(strComputer)
    intDS = DisableServices(strComputer)
    If intKP = 0 Then
      TrapProcesses strComputer
    Else
      strMessage2 = vbCrLf & "  Unable to monitor target processes."
      WScript.Echo strMessage2
      WriteTextFile g_strOutputFile, strMessage2
    End If
    If intDS = 0 Then
      TrapServices strComputer
    Else
      strMessage3 = vbCrLf & "  Unable to monitor target services."
      WScript.Echo strMessage3
      WriteTextFile g_strOutputFile, strMessage3
    End If
  Else
    HandleError(strComputer)
  End If

Next

strMessage4 = VbCrLf & _
 "     -----------------------------------------------------------------" & _
 VbCrLf & VbCrLf & "In monitoring mode ... Ctrl+C to end"
WScript.Echo strMessage4
WriteTextFile g_strOutputFile, strMessage4
 
Do
   WScript.Sleep 1000
Loop

'******************************************************************************

Function KillProcesses(strHost)
'Terminate processes on list on specified machine.

On Error Resume Next

strPQuery = "SELECT * FROM Win32_Process"
intTPFound = 0
intTPKilled = 0
strPData = ""

strPData = strPData & vbCrLf & "  Checking for target processes ..."
Set colProcesses = g_objWMIService.ExecQuery(strPQuery)
If Err = 0 Then
  For Each objProcess in colProcesses
    For Each strTargetProc In g_arrTargetProcs
      If LCase(objProcess.Name) = LCase(strTargetProc) Then
        intTPFound = intTPFound + 1
        strPData = strPData & vbCrLf & _
         "  Process Name: " & objProcess.Name & vbCrLf & _
         "    PID: " & objProcess.ProcessID & vbCrLf & _
         "    Time: " & Now
        intReturn = objProcess.Terminate
        If intReturn = 0 Then
          strPData = strPData & vbCrLf & "    Terminated"
          intTPKilled = intTPKilled + 1
        Else
          strPData = strPData & vbCrLf & "    Unable to terminate"
        End If
      End If
    Next
  Next
  strPData = strPData & vbCrLf & "  Target processes found: " & intTPFound
  If intTPFound <> 0 Then
    strPData = strPData & vbCrLf & "  Target processes terminated: " & intTPKilled
  End If
  intTPUndead = intTPFound - intTPKilled
  If intTPUndead <> 0 Then
    strPData = strPData & vbCrLf & _
     "  ALERT: Target processes not terminated: " & intTPUndead
  End If
  KillProcesses = 0
  WScript.Echo strPData
  WriteTextFile g_strOutputFile, strPData
Else
  HandleError(strHost)
  KillProcesses = 1
End If

End Function

'******************************************************************************

Function DisableServices(strHost)
'Disable services on list on specified machine.

On Error Resume Next

strSQuery = "SELECT * FROM Win32_Service"
intTSFound = 0
intTSStopped = 0
intTSDisabled = 0
strSData = ""

strSData = strSData & vbCrLf & "  Checking for target services ..."
Set colServices = g_objWMIService.ExecQuery(strSQuery)
If Err = 0 Then
  For Each objService in colServices
    For Each strTargetSvc In g_arrTargetSvcs
      If LCase(objService.Name) = LCase(strTargetSvc) Then
        intTSFound = intTSFound + 1
        strSData = strSData & VbCrLf & _
         "  Service Name: " & objService.Name & VbCrLf & _
         "    Status: " & objService.State & VbCrLf & _
         "    Startup Type: " & objService.StartMode & VbCrLf & _
         "    Time: " & Now
        If objService.State = "Stopped" Then
          strSData = strSData & VbCrLf & "    Already stopped"
          intTSStopped = intTSStopped + 1
        Else
          intStop = objService.StopService
          If intStop = 0 Then
            strSData = strSData & VbCrLf & "    Stopped service"
            intTSStopped = intTSStopped + 1
          Else
            strSData = strSData & VbCrLf & "    Unable to stop service"
          End If
        End If
        If objService.StartMode = "Disabled" Then
          strSData = strSData & VbCrLf & "    Already disabled"
          intTSDisabled = intTSDisabled + 1
        Else
          intDisable = objService.ChangeStartMode("Disabled")
          If intDisable = 0 Then
            strSData = strSData & VbCrLf & "    Disabled service"
            intTSDisabled = intTSDisabled + 1
          Else
            strSData = strSData & VbCrLf & "    Unable to disable service"
          End If
        End If
      End If
    Next
  Next
  strSData = strSData & vbCrLf & "  Target services found: " & intTSFound
  If intTSFound <> 0 Then
    strSData = strSData & vbCrLf & _
     "  Target services stopped: " & intTSStopped & vbCrLf & _
     "  Target services disabled: " & intTSDisabled
  End If
  intTSRunning = intTSFound - intTSStopped
  intTSAbled = intTSFound - intTSDisabled
  If intTSRunning <> 0 Then
    strSData = strSData & vbCrLf & _
     "  ALERT: Target services not stopped: " & intTSRunning
  End If
  If intTSAbled <> 0 Then
    strSData = strSData & vbCrLf & _
     "  ALERT: Target services not disabled: " & intTSAbled
  End If
  DisableServices = 0
  WScript.Echo strSData
  WriteTextFile g_strOutputFile, strSData
Else
  HandleError(strHost)
  DisableServices = 1
End If

End Function

'******************************************************************************

Sub TrapProcesses(strHost)

On Error Resume Next

strPAsyncQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"

'Trap asynchronous events.
g_objWMIService.ExecNotificationQueryAsync SINK1, strPAsyncQuery
If Err = 0 Then
  strTPMessage1 = vbCrLf & "  Monitoring target processes."
  WScript.Echo strTPMessage1
  WriteTextFile g_strOutputFile, strTPMessage1
Else
  HandleError(strHost)
  strTPMessage2 = vbCrLf & "  Unable to monitor target processes."
  WScript.Echo strTPMessage2
  WriteTextFile g_strOutputFile, strTPMessage2
End If

End Sub

'******************************************************************************

Sub TrapServices(strHost)

On Error Resume Next

strSAsyncQuery = "SELECT * FROM __InstanceModificationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Service'"

'Trap asynchronous events.
g_objWMIService.ExecNotificationQueryAsync SINK2, strSAsyncQuery
If Err = 0 Then
  strTSMessage1 =  vbCrLf & "  Monitoring target services."
  WScript.Echo strTSMessage1
  WriteTextFile g_strOutputFile, strTSMessage1
Else 
  HandleError(strHost)
  strTSMessage2 =  vbCrLf & "  Unable to monitor target services."
  WScript.Echo strTSMessage2
  WriteTextFile g_strOutputFile, strTSMessage2
End If

End Sub

'******************************************************************************

Sub SINK1_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap process events asynchronously.

strSink1Data = ""

For Each strTargetProc In g_arrTargetProcs
  If LCase(objLatestEvent.TargetInstance.Name) = LCase(strTargetProc) Then
    strSink1Data = strSink1Data & VbCrLf & "Target process on: " & _
     objLatestEvent.TargetInstance.CSName & VbCrLf & _
     "  Name: " & objLatestEvent.TargetInstance.Name & VbCrLf & _
     "  PID: " & objLatestEvent.TargetInstance.ProcessID & VbCrLf & _
     "  Time: " & Now
'Terminate process.
    intReturn = objLatestEvent.TargetInstance.Terminate
    If intReturn = 0 Then
      strSink1Data = strSink1Data & VbCrLf & "  Terminated process."
    Else
      strSink1Data = strSink1Data & VbCrLf & _
       "  Unable to terminate process. Return code: " & intReturn
    End If
  End If
Next

Wscript.Echo strSink1Data
WriteTextFile g_strOutputFile, strSink1Data

End Sub

'******************************************************************************

Sub SINK2_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap service events asynchronously.

strSink2Data = ""

For Each strTargetSvc In g_arrTargetSvcs
  If LCase(objLatestEvent.TargetInstance.Name) = LCase(strTargetSvc) Then
    strSink2Data = strSink2Data & VbCrLf & "Target service on: " & _
     objLatestEvent.TargetInstance.SystemName & VbCrLf & _
     "  Name: " & objLatestEvent.TargetInstance.Name & VbCrLf & _
     "  Status: " & objLatestEvent.TargetInstance.State & VbCrLf & _
     "  Startup Type: " & objLatestEvent.TargetInstance.StartMode & VbCrLf & _
     "  Time: " & Now
'Stop service.
    If objLatestEvent.TargetInstance.State = "Stopped" Then
      strSink2Data = strSink2Data & VbCrLf & "  Already stopped"
    Else
      intStop = objLatestEvent.TargetInstance.StopService
      If intStop = 0 Then
        strSink2Data = strSink2Data & VbCrLf & "  Stopped service"
      Else
        strSink2Data = strSink2Data & VbCrLf & "  Unable to stop service"
      End If
    End If
'Disable service.
    If objLatestEvent.TargetInstance.StartMode = "Disabled" Then
      strSink2Data = strSink2Data & VbCrLf & "  Already disabled"
    Else
      intDisable = objLatestEvent.TargetInstance.ChangeStartMode("Disabled")
      If intDisable = 0 Then
        strSink2Data = strSink2Data & VbCrLf & "  Disabled service"
      Else
        strSink2Data = strSink2Data & VbCrLf & "  Unable to disable service"
      End If
    End If
  End If
Next

Wscript.Echo strSink2Data
WriteTextFile g_strOutputFile, strSink2Data

End Sub

'******************************************************************************

Function ReadTextFile(strFileName)
'Read lines of text file and return array with one element for each line.

On Error Resume Next

Const FOR_READING = 1
Dim arrLines()

Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFilename) Then
  Set objTextStream = objFSO.OpenTextFile(strFilename, FOR_READING)
Else
  strRTFMessage1 = VbCrLf & "Input text file " & strFilename & " not found."
  Wscript.Echo strRTFMessage1
  WriteTextFile g_strOutputFile, strRTFMessage1
  WScript.Quit(1)
End If

If objTextStream.AtEndOfStream Then
  strRTFMessage2 = VbCrLf & "Input text file " & strFilename & " is empty."
  Wscript.Echo strRTFMessage2
  WriteTextFile g_strOutputFile, strRTFMessage2
  WScript.Quit(2)
End If

Do Until objTextStream.AtEndOfStream
  intLineNo = objTextStream.Line
  ReDim Preserve arrLines(intLineNo - 1)
  arrLines(intLineNo - 1) = objTextStream.ReadLine
Loop

objTextStream.Close

ReadTextFile = arrLines

End Function

'******************************************************************************

'Write or append data to text file.
Sub WriteTextFile(strFileName, strOutput)

On Error Resume Next

Const FOR_APPENDING = 8

'Open text file for output.
Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFileName) Then
  Set objTextStream = objFSO.OpenTextFile(strFileName, FOR_APPENDING)
Else
  Set objTextStream = objFSO.CreateTextFile(strFileName)
End If

'Write data to file.
objTextStream.WriteLine strOutput
objTextStream.WriteLine

objTextStream.Close

End Sub

'******************************************************************************

Sub HandleError(strHost)
'Handle errors.

strError = VbCrLf & "  ERROR on " & strHost & VbCrLf & _
 "    Number: " & Err.Number & VbCrLf & _
 "    Description: " & Err.Description & VbCrLf & _
 "    Source: " & Err.Source
WScript.Echo strError
WriteTextFile g_strOutputFile, strError
Err.Clear

End Sub

スクリプトの先頭では、スクリプトを異なる環境で実行する際に変更が必要になる可能性のあるすべての変数が "変更ブロック (Change block)" というセクションにまとめられていることに注意してください。これにより、変数の現在値を簡単に把握できます。

このスクリプトにこれまでのスクリプトから取り入れるもう 1 つの方法は、次のコードを使用して、異なる 2 つのイベント シンクを作成することです。1 つはプロセス イベント用、もう 1 つはサービス イベント用です。

Set SINK1 = WScript.CreateObject("WbemScripting.SWbemSink","SINK1_")
Set SINK2 = WScript.CreateObject("WbemScripting.SWbemSink","SINK2_")

このコードでは、さまざまなイベントを処理するために 2 つの異なるサブルーチン SINK1_OnObjectReady と SINK2_OnObjectReady も必要です。一度の同期ですべてのイベントをできるようにする一方で、イベント ソースを区別することにより、コーディングの概念が簡単になります。

このスクリプト本体のメイン ロジックは、リスト 4 の対象プロセスを終了してから新しいプロセスを監視するロジックとほとんど違いません。ただし、ここではプロセスとサービスの両方を監視しているため、互いに独立した 2 つのロジックの並列パスが形成されます。

スクリプトでは、まず、コンピュータの一覧をループし、各コンピュータの WMI にバインドします。これは、WMI サービスがコンピュータで実行されていることを突き止めるだけでなく、ネットワークの接続性も確認します。

多数のコンピュータに対してさらにパフォーマンスを向上させるために、各コンピュータに ping を発行する関数を追加しました。ただし、WMI はコンピュータにバインドできない場合、数秒以内にエラーを返すので、ここでの目的には適していません。

その後、現在のコンピュータで、KillProcesses と DisableServices を連続して呼び出します。これらは両方ともきわめて迅速に実行されます。このロジックでは、KillProcesses の戻り値を確認してから、DisableServices の戻り値を確認します。これらの各関数は、ExecQuery がそのコンピュータでエラーをスローすると 1 を、ExecQuery 正常に実行されると 0 を返します。対象プロセスまたはサービスが見つからなかった場合、または一部が見つかっても停止されなかった場合は、続けてそれぞれの監視関数を呼び出します。対象プロセスとサービスのどちらが最初に検出されても、両方を監視する必要があるので、プロセス用のロジックとサービス用のロジックに依存関係がないことに意味があります。

KillProcesses と DisableServices が現在のコンピュータを照会できる場合に限り、次に TrapProcesses と TrapServices が呼び出されます。これらのサブルーチンはそれぞれ、非同期クエリを実行して独自のイベント シンクを作成し、それぞれのイベントをキャッチします。

リスト 7 で使用した他のすべてのプロシージャについては既に説明しました。また、KillProcesses、DisableServices、TrapProcesses、および TrapServices についても既に説明しました。ReadTextFileWriteTextFile、および HandleError は、以前のコラムで使用しました。

このスクリプトでの ReadTextFile 関数の使用法は、スクリプトをプロシージャに分割する 1 つの理由を示しています。ReadTextFile を使用すると、3 つの各入力ファイルを同じ関数で開くことができます。この関数は、各ファイルを配列に読み取り、配列を返します。その後、この方法で取得されたコンピュータ、プロセス、およびサービスの配列が他のコードで使用されます。複数のテキスト ファイルを読み取るコードを 1 つの関数にカプセル化しないと、この処理をスクリプト本体で 3 回繰り返す必要があります。

スクリプトはコマンドシェル ウィンドウとテキスト ログ ファイルの両方に結果を出力するので、すべてのプロシージャは結果を文字列として書き込み、スクリプトの進行につれて新しい情報を追加します。次に例を示します。

strPData = strPData & vbCrLf & "    Terminated"

上記のプロシージャは、インデント用の空白と "Terminated" を含む文字列を、strPData に既に存在する文字列 (この場合は、プロセスの説明) に追加しています。

各プロシージャの最後に、次のような行があります。

WScript.Echo strPData
  WriteTextFile g_strOutputFile, strPData

この行により、コマンド プロンプトに最終的な文字列が表示され、その文字列 strPData をテキスト ログ ファイルに追加する WriteTextFile が呼び出されます。この累積的な出力のコレクションを有効にするには、次のコードで WriteTextFile を使って追加先のログ ファイルを開きます。

Set objTextStream = objFSO.OpenTextFile(strFileName, FOR_APPENDING)

つまり、ログ ファイルに書き込まれる累積された文字列は、既存のコンテンツに上書きするのではなく、既存のコンテンツに追加されます。

仮のログ ファイルを次に示します。

リスト 8 : テキスト ファイルの出力ログ

Host: sea-wks-1

  Checking for target processes ...
  Process Name: freecell.exe
    PID: 3247
    Time: 7/21/2005 4:22:40 PM
    Terminated
  Target processes found: 1
  Target processes terminated: 1


  Checking for target services ...
  Service Name: Alerter
    Status: Running
    Startup Type: Manual
    Time: 7/21/2005 4:22:41 PM
    Stopped service
    Disabled service
  Target services found: 1
  Target services stopped: 1
  Target services disabled: 1


  Monitoring target processes.


  Monitoring target services.


Host: sea-wks-2


  Checking for target processes ...
  Target processes found: 0


  Checking for target services ...
  Service Name: SCardSvr
    Status: Stopped
    Startup Type: Disabled
    Time: 7/21/2005 4:22:42 PM
    Already stopped
    Already disabled
  Target services found: 1
  Target services stopped: 1
  Target services disabled: 1


  Monitoring target processes.


  Monitoring target services.


Host: sea-srv-1


  ERROR on sea-srv-1
    Number: 462
    Description: The remote server machine does not exist or is unavailable
    Source: Microsoft VBScript runtime error


     -----------------------------------------------------------------

In monitoring mode ... Ctrl+C to end

Target process on: sea-wks-2
  Name: calc.exe
  PID: 3940
  Time: 7/21/2005 4:21:55 PM
  Terminated process.


Target service on: sea-wks-1
  Name: SCardSvr
  Status: Stopped
  Startup Type: Manual
  Time: 7/21/2005 4:23:05 PM
  Already stopped
  Disabled service


Target service on: sea-wks-2
  Name: WZCSVC
  Status: Stopped
  Startup Type: Disabled
  Time: 7/21/2005 4:23:06 PM
  Already stopped
  Already disabled


Target process on: sea-wks-1
  Name: notepad.exe
  PID: 3040
  Time: 7/21/2005 4:21:50 PM
  Terminated process.


Target service on: sea-wks-1
  Name: Alerter
  Status: Stopped
  Startup Type: Manual
  Time: 7/21/2005 4:23:19 PM
  Already stopped
  Disabled service


Target service on: sea-wks-2
  Name: WZCSVC
  Status: Stopped
  Startup Type: Disabled
  Time: 7/21/2005 4:23:20 PM
  Already stopped
  Already disabled
^C

後記

このスクリプトは、ファイルやレジストリ エントリの削除、ユーザーや共有の削除などの作業を実行するために拡張できます。リスト 7 をモジュール構成にしたことで、これらの作業を実行するスクリプトに関数を比較的容易に追加できるようになりました。しかし、Dr. Scripto は過酷な作業で疲れ、またもや机の上で寝てしまいました。ここまで読んでくださった忍耐強い読者の皆さんも、おそらく休憩されたいころでしょう。

一方、このコラムで活気付けられて、この種のスクリプトで実行できる他の種類の操作のコード例を知りたくなったら、「TechNet System Administration Scripting Virtual Lab」(英語) を参照してください。