以非同步事件監視/控制害蟲程式

作者: 作者:Microsoft Scripting Guys

Doctor Scripto 辛勤工作

《Doctor Scripto 指令碼工廠》(Doctor Scripto's Script Shop) 可將簡單的指令碼範例融合成更複雜的指令碼,解決實務系統管理指令碼處理問題。這些問題大多都是讀者親身所經歷分享而來。Doctor Scripto 的小發明並非絕對周全,也不保證百毒不侵。但確實讓您清楚瞭解到:如何從可重複使用的程式碼模組建置高效能指令碼、處理錯誤並傳回錯誤碼、從不同來源取得輸入和輸出、對多部電腦執行處理,並執行您想要以生產指令碼處理的作業。

希望您會覺得這些專欄和指令碼對您有所幫助。有任何寶貴的意見,都請您不吝賜告。如果您已想出解決這些問題的任何其他方法,或者有任何希望日後能夠探討的主題,歡迎讀者分享,大家彼此切磋琢磨。

如需前期專欄的過往文件,請參閱《Doctor Scripto 指令碼工廠過往文件》(英文)。

(附註:感謝 Alain Lissoir、Chris Scoville、Mary Gray 和 Steve Lee 提供寶貴建議,出手襄助。)

本页内容

利用非同步方法監視多部電腦上的處理和服務
WMI 非同步方法
檢查正在執行的處理序
找出處理序並加以終止
使用非同步事件處理截獲處理序
終止之後監視目標處理序
停止及停用服務
使用非同步事件處理監視服務
保護電腦不受有害處理序和服務破壞
Postscript


利用非同步方法監視多部電腦上的處理和服務

現在是病毒、蠕蟲、廣告軟體和間諜軟體等肆虐的時機。由於他們是萬眾矚目的大紅人,全都成了滾石雜誌的頭號封面人物,好啦!我承認有點跨張,其實是登載在紐約時報上,照片中有一個博士由於被廣告軟體重重包圍,弄得她無處下手,不再點按任何出現的畫面。時代雜誌也訪問了一位網際網路業者的決策主管。他曾憤而將遭受感染的電腦扔進垃圾筒中,因為他認為買新電腦比清除各式病毒感染的代價更低,也更快速。

別冤枉我們喔!這可不是 Scripting Guys 病毒撰寫學校的招生廣告。也許在一些無聊的 16 歲孩子看來,駭客攻擊是很酷的事情,但對咱門賴以維生的資訊業界都已經造成極其巨大的損害,更別提其他使用電腦的所有用戶所遭受的損失!

我們就不厭其詳地先從最明顯的情況討論起 (Scripting Guys 的專長之一):結合防病毒和防間諜軟體應用程式且周延全面的防護安全策略是無可取代的。但是在設置了廣泛防護的安全策略以後,指令碼仍然能夠發揮作用,可執行相關的掃蕩作業以及其他必須自訂或快速自動化的支援工作。在您的防毒軟體廠商能夠提供您最新、最熱門的蠕蟲病毒碼之前,您該怎麼辦?而由其他來源 (例如,憤懣不平的內部人員) 所製造出來而商品化軟體無法解決的問題,又該怎麼辦?還有,如果問題只是讓人不堪其擾而並非惡意破壞,例如受歡迎的遊戲軟體在公司網路上佔用了太多頻寬,那又該怎麼辦?

在這種種情況下,如果能夠迅速揮灑出指令碼,監視所不想要軟體出現的徵兆,而且一經發現立即加以壓制,這樣不是很棒嗎?

而且並不是只有我們在思考這些問題:南加州社區大學系統的系統管理員 Sergio,提出下列建議構想:

「何不撰寫指令碼,專門揪出與病毒、蠕蟲或間諜軟體連接的惡意服務?另外建立並維護一份清單,把這些作惡的服務名稱給列下。然後在跨網域的多部電腦上進行這項工作。」

來自印度的 Chad 將他電腦上可以揪出不明處理序的指令碼傳給我們分享。

Doctor Scripto 真心關切並認真考慮這項議題,從此就成為狗仔隊一樣,專門追逐害蟲程式:經常死追著行為不軌的程式碼,揭發出它們所有不為人知的醜陋秘密。但是他可不是去拍照蒐證,而是直接將程式碼關閉、鎖住、加以驅逐 (別以為他只是個不傷人的小小虛擬 Scripting Guy,他可是有兩下字的喔)。

為了進行這項吃力不討好的工作,Doctor 運用了一些上期專欄中粗略探討的指令碼處理功能。該文討論如何執行處理序,然後追蹤其執行期間,作法跟修補程式、更新程式、安裝程式或其他公用程式差不多。本文不會探討如何建立處理序。我們將重點放在如何以各種不同的方式監視事件,尤其是能夠在多部電腦上有效運作的方式,也就是所謂的非同步事件監視。

我們要使用非同步事件監視,以截獲特定的有害處理序和服務,將它停止或停用。這些指令碼中所闡述的原則也可以稍加修改,用來揪出並解決不請自來的外來程式碼可能會產生的其他徵兆。

WMI 非同步方法

Doctor Scripto 在上期專欄中,說明如何在 SWbemServices 物件的 ExecNotificationQuery 方法呼叫 WMI Scripting API,藉此追蹤指令碼執行的處理序。

但是如果我們想在多部電腦上監視事件使用者種方法,會有一些問題。WMI 方法可以是同步、半同步或非同步。ExecNotificationQuery 是同步或半同步 (VBScript 的預設值) 的事件監視方法。同步或半同步方法執行是這樣運作的:ExecNotificationQuery 替第一部電腦傳回的 SWbemEventSource 物件會懸置並等待該部電腦上的事件觸發其 NextEvent 方法,不讓指令碼繼續監視其他電腦上的事件。

遇到這種情況,我們的解決方法是替名單上每一部電腦另外產生新的指令碼,然後將處理序要監視的電腦名稱和處理序 ID 傳送給該指令碼。這個解決方法雖然不錯,但是必須使用多套指令碼,就顯得有些小題大作。

Doctor Scripto 於是大膽深入探究 WMI 內部,開發出不太常用但更簡潔的手法,可在多部電腦上執行事件監視。咱們 Scripting Guys 可是奉簡潔為寫程式的圭杲。

非同步方法有點像電視上的大自然節目。攝影師潛入荒野設置隱藏式攝影機,配上絆網觸發器,錄下當地來來往往的野生動物。攝影師不必在場按下快門,因為絆網可以代勞,由攝影機留下影像。不過在本專欄中,我們所要追蹤的可不是行蹤不定的狒狒,也不是大羚羊,而是可疑的處理序和服務。

SWbemServices 物件是 WMI Scripting API 以及本文大部份範例指令碼中最勞苦功高的成員之一,它包含幾個非同步方法,比起同步或半同步同級成員,可是擔負著長期抗戰的任務:ExecQuery 有 ExecQueryAsync,InstancesOf 有 InstancesOfAsync,而且 Doctor 很高興 ExecNotificationQuery 有 。

同步或非同步方法採取更直接的手法,執行動作,然後暫停,了解狀況之後再傳回結果,而非同步方法則是儘速進行查詢並傳回結果。但是非同步方法會在事後留下所謂的接收器,這是一種特殊物件,專門等候接收非同步方法的結果。WMI Scripting API 包含特殊類型的物件 SWbemSink,可為非同步方法執行這項功能。

您可以將接收器視為方法所留下的遙控攝影機及感應器,隨時向方法回報所要偵查的持續進行活動。

使用 ExecNotificationQueryAsync 方法時,結果就等於事件,而呼叫此方法的指令碼就成為 WMI 所參照的事件取用者。SWbemSink 物件這裡擔任事件接收器的任務,在目標電腦上等候著查詢中所指定的事件發生。一旦發生此類事件,就會觸發 SWbemSink 的 OnObjectReady 事件,由指令碼截獲並進行處理。(您可以在 WMI SDK 中找到更多有關非同步事件監視的完整資訊。)

執行 ExecNotificationQueryAsync


接收器等待 OnObjectReady


它們拖累我的授權部門:

其實用一個 SWbemSink 物件就可以監視三部遠端電腦,並在執行指令碼的本端電腦上執行個體化。

使用 ExecNotificationQueryAsync 方法在多部電腦上監視事件的最大好處是:它可以使用事件接收器監視相同指令碼內的所有電腦,而不必分別為每部電腦啟動指令碼。

我們在上上期專欄《現在是午夜兩點,你知道處理序在哪裡嗎?》(It's 2 a.m. Do you know where your processes are?) 中探討了事件查詢的基本概念。附帶一提,如果您初次聽到 WMI 事件,請參閱 Scripting Guys 網路廣播,《預防小招數:簡介 WMI 事件》(英文)。

本專欄下文中將利用非同步事件監視的優點。現在讓我們先瞧瞧 Dr. Scripto 監視作業精采演出中較簡單、也較熟悉的指令碼處理工作。

檢查正在執行的處理序

如果擔心電腦上可能會有一些不想要的處理序在執行,可以先以最基本的指令碼為基礎加以修正變化,再進行檢查。如果您參加過 Scripting Guys 網路廣播或專題研討,或者曾經閱讀《Windows 2000 Scripting Guide》(英文),可能已經看過列出電腦上所執行處理序的簡單指令碼之一。

下列指令碼則是再進一步:它會以處理序名單作為依據,檢查這些處理序,並顯示相符的處理序。我們使用 Calculator、Notepad 和 Freecell 作為惡意處理序的代理程式來測試這個指令碼時,會以這些應用程式,各開啟幾個視窗。

[列表 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 函式,讓所有處理序名稱都是小寫字母,這樣就不致於因為其中一個名稱使用不同大小寫的關係而漏檢。找到相符項目時就會顯示處理序的名稱。別忘了!許多可執行檔可能都會有多個名稱相同的處理序同時在執行。

好,既然已經識別出不希望在電腦上浪費資源的處理序,現在又該怎麼辦呢?

找出處理序並加以終止

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 主題中)。

使用非同步事件處理截獲處理序

仔細想想,第二種指令碼只不過是停止處理序而已,並未刪除執行處理序的可執行檔。那麼 Freecell 劣根性不會再度啟動作怪嗎?而且要是病毒運用一些迂迴曲折的手段,再度自行啟動興風作浪,那怎麼辦?

您可以刪除可執行檔,如果是遊戲軟體的話那得大費周章了。在其他情況下,您也可能要移除檔案。您可以編寫指令碼,執行多種不同動作,應付處理各種情況,Doctor Scripto 雖然也很想在專欄中一一探討解決,但可不能讓它一發不可收拾,洋洋灑灑地寫成一本小說。所以我們先集中精力,專注於一種方式,阻止有害的處理序蠢蠢欲動。

以動作片術語來說,我們要「終結」那些極端偏激的處理序。(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 所消耗的處理頻寬較小,而且如果在遠端電腦上執行所產生的網路活動也會較少。當然也會有壞處,也會提供更多時間,讓蠢蠢欲動的有害處理序乘機執行,而無法立即加以終止。特定環境中的最佳設定必須在該環境下仔細地反覆測試,才能決定。

使用非同步方法時,指令碼必須建立前文所討論的接收器。若要建立接收器,必須呼叫 Windows Script Host 方法 CreateObject,並傳遞兩個參數給它:程式設計 ID WbemScripting.SWbemSink 和識別碼 (在本範例中為 SINK_)。這兩個參數將於稍後在指令碼中呼叫時,附加在 SWbemSink 方法 (如 OnObjectReady) 開頭。


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


我們所建立的接收器會有事件與其相關聯,在本範例中主要就是 OnObjectReady。只要有非同步呼叫提供的物件可供接收器使用時,就會觸發 OnObjectReady,在本範例中也就是只要建立處理序時。若要處理這些事件物件,必須撰寫一個副程式。這個副程式必須以 CreateObject 呼叫 (SINK_) 中所指定前置詞為開頭,並以事件名稱為結尾。因此截獲事件的副程式就稱為 SINK_OnObjectReady。

連接至 WMI 並呼叫 ExecNotificationQueryAsync 方法時,要傳遞兩個參數:

  • 剛建立的接收器物件名稱,在本範例中即為 SINK (相當實在的名字,沒有必要在這裡花費心思講求創意)。

  • 要執行的 WQL 事件查詢,在本範例中為 "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'"

在本範例中,我們要求 WMI 在發現任何建立處理序的事件時發出通知,而且每間隔 1 秒即進行檢查。執行上述作業以後,指令碼就進入永無止盡的迴圈,恭候事件發生。若要結束迴圈,請按 Ctrl+C。

引發與查詢中條件相符的事件時,SINK_OnObjectReady 即傳回一個叫做 objLatestEvent 的輸出參數,是代表新事件的物件 (在本指令碼中我們不會使用另一個參數 objAsyncContext)。在前兩期專欄中我們討論過同步與半同步查詢,情形相同,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。

停止及停用服務

現在我們已經有簡單可運作的指令碼,可以對付處理序,但還根本沒談到有關服務的任何情況。許多應用程式是以服務的形式而不是以處理序執行,或是在處理序之外執行。那我們應該要如何使用對付處理序的手法解決有害的服務呢?

服務的解決方案與處理序指令碼極其類似。我們的目標是停用不想要的服務,以阻止它們再度啟動,但是在這裡情況就比較複雜:如果我們在執行時停用服務,還是無法停止服務。將來它只是變更啟動型態而已。因此我們的指令碼必須能夠停止服務,同時也停用服務,才能讓它無法再度啟動。

如果您試用這段指令碼,一定要注意所測試之各個服務的 Status 和 Startup Type,再執行指令碼,而在您完成作業以後,讓它們回復到原來的狀況。本範例使用作業系統服務 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_Service 類別而不用 Win32_Process。方法會傳回安裝在電腦上的所有服務集合。

服務的 Status 和 Startup Type (由 Services 嵌入式管理單元所使用的名稱) 是獨立的:如果服務是在執行時停用,它會繼續執行。若未執行時,則服務可以停止,但不能停用。因此必須停止這些服務,同時也加以停用。

若要同時執行這兩項工作,首先指令碼要檢查服務是否正在執行。Win32_Service 使用嵌入式管理單元稱之為 Status 的 State 屬性,讓指令碼檢查 State 屬性。如果值不是「Stopped」,指令碼即呼叫 StopService 方法。

然後指令碼再檢查 StartMode 屬性,嵌入式管理單元稱之為 Startup Type。如果不是「Disabled」,則指令碼會呼叫 ChangeStartMode 方法,傳遞「Disabled」給它作為唯一的參數。

指令碼執行完成以後,所有目標服務都應該已經停止並停用。

使用非同步事件處理監視服務

與處理序相同,我們不能冀望惡意服務會永遠停止並停用。下列指令碼就是說明如何使用非同步事件查詢,密切注意名單中的服務,確保它們無法再度啟動。

這種指令碼是不停歇地持續執行。若要結束,請按 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] 中的類似,指令碼停止並停用服務。

在本範例中,於 TargetInstance 為 Win32_Service 而不是 Win32_Process 的情形下,查詢是傳遞篩選條件給 ExecNotificationQueryAsync。它也選取了 __InstanceModificationEvent 的執行個體,而不是 [列表 3] 指令碼中的 __InstanceCreationEvent。這是因為所有可疑的服務都已經安裝了:變更啟動模式的事件,其啟動是執行個體的修正而非建立。

截獲目標事件時,SINK_OnObjectReady 子函式必須停止服務,同時加以停用。它是透過呼叫 Win32_Service 的兩個方法 StopService 和 ChangeStartMode,並將後者傳遞給參數 "Disabled",完成停止及停用工作。

保護電腦不受有害處理序和服務破壞

現在已經檢視了完成各項工作所需的各段程式碼,就該讓 Doctor Script 開動焊炬,將本專欄中先前討論的所有程式碼片段全都融合在一起,組成一個多用途的綜合指令碼。

最後一段指令碼是:

  • 從文字檔取得處理序名單。

  • 從文字檔取得服務名單。

  • 從文字檔取得電腦名單。

  • 在每一部電腦上:

    • 檢查是否有名單上的處理序。

    • 如果找到任何處理序,立即加以終止。

    • 檢查是否有名單上的服務。

    • 如果找到任何服務,立即加以停止並停用。

    • 拋出非同步事件接收器,以監視名單上所有處理序的 __InstanceCreationEvent。

    • 如果其中有任何處理序啟動,即加以終止。

    • 拋出非同步事件接收器,以監視名單上所有服務的 __InstanceModificationEvent。

    • 如果其中有任何服務啟動,即加以停止並停用。

  • 將結果記錄至文字記錄檔,並顯示在命令殼層視窗中。

指令碼邏輯 - 最後一段指令碼

按照本專欄慣例,這段指令碼是可供您加工利用的擴充式範本。您必須加以自訂,以便該它能在您的環境中運作,您可能要稍加改善及增訂。我們已經在一小組執行 Windows XP 和 Windows Server 2003 的電腦上進行過測試,但是並未徹底地對許多電腦或在異質性環境中進行測試。

下面是供輸入的文字檔範例。所有檔案都必須位於指令碼所在的目錄中。您可以在指令碼開頭的變更區塊中,變更檔名和路徑。

下列檔案在範例指令碼中命名為 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" 的區段中。讓您輕易就能掌握目前的變數值。

這段指令碼所採取與先前指令碼不同的另一種手法是,分別建立兩個互不相屬的事件接收器,一個供處理序事件使用,另一個供服務事件使用,程式碼如下:


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


在這種情形下,也必須要有兩個不同的子函式,才能處理不同的事件,SINK1_OnObjectReady 和 SINK2_OnObjectReady。雖然是可以在一次同步作業中收集所有事件,接下來區分事件來源的工作,使得這種手法在概念上比較容易著手撰寫程式碼。

本指令碼主體中的主邏輯與 [列表 4] 中的並沒有太大差異:終止目標處理序,然後監視新的處理序。但是在此處,指令碼是同時監視處理序和服務,因此它是建立兩條平行的邏輯路徑,彼此獨立互不相干。

指令碼首先依電腦名單執行迴圈作業,並嘗試繫結至每一部電腦上的 WMI。這項作業可檢查網路連接,並確定電腦上確實在執行 WMI 服務。

若要在大量電腦上執行時取得更快速的效能,可以加入函式,先對每一部電腦進行偵測。如果 WMI 無法繫結至電腦時,會在幾秒之內傳回錯誤,如此即可達到我們所要的目的。

然後,指令碼會在目前電腦上,接續呼叫 KillProcesses 和 DisableServices,兩者應該都會快速地執行。邏輯會先檢查 KillProcesses 所傳回的值,再檢查 DisableServices 所傳回的值。如果 ExecQuery 在該電腦上擲回錯誤,這些函式每一個都會傳回 1;如果 ExecQuery 執行成功,則會傳回 0。即使找不到目標處理序或服務,或者找到一些但並未停止,邏輯也會繼續呼叫相對應的監視函式。處理序的邏輯獨立於服務的邏輯之外是很有道理的,因為不管一開始是否找到任何處理序或服務,指令碼都應該要繼續監視目標處理序和服務。

只要 KillProcesses 和 DisableServices 能夠查詢目前的電腦,接著就會呼叫 TrapProcesses 和 TrapServices。這些子函式各個都會執行非同步查詢,並拋出本身所擁有的事件接收器,分別截獲相對應的事件。

您已經看過用在 [列表 7] 中的所有其他程序,KillProcesses、DisableServices、TrapProcesses 和 TrapServices 都已經在上文中說明過。ReadTextFileWriteTextFileHandleError 都已經在前期的專欄中用過了。

本指令碼使用 ReadTextFile 函式的方式說明了將指令碼分成幾個程序的理由。ReadTextFile 可以讓指令碼用相同的函式,開啟三個輸入檔,然後讀出各檔,送入陣列,再傳回陣列。再由其他程式碼使用以這種方式擷取之電腦、處理序和服務的陣列。如果不封裝程式碼,以讀取函式中的文字檔,就必須在指令碼主體中重複三次。

因為指令碼會將結果輸出到命令殼層視窗和文字記錄檔,所有程序都會將結果寫成字串,同時一邊附加新資訊。例如:


strPData = strPData & vbCrLf & "    Terminated"


附加字串,其中包含縮排的空格,並進行「Terminated」至已在 strPData 中的項目,在本範例中是處理序的描述。

在各個程序結尾,下列程式碼行:


WScript.Echo strPData
  WriteTextFile g_strOutputFile, strPData


會在命令提示下顯示已完成的字串,並呼叫 WriteTextFile,以便將字串 strPData 附加至文字記錄檔中。若要啟用此累計的輸出集合,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


Postscript

我們可以擴充此指令碼,執行刪除檔案或登錄項目以及移除使用者或共用等工作。[列表 7] 的模組結構讓它相當容易就能加入執行這些工作的函式。但是 Dr. Scripto 又因精疲力盡而在他書桌上睡著了,不過耐心的讀者,要是您已經跟隨本文走到這地步,也該需要休息了。

不過話說回來,如果本專欄激發出您充沛的活力,想要再看一些程式碼範例,了解這類指令碼能夠執行的其他工作,請參閱《TechNet 系統管理員指令碼虛擬實驗室》(英文)。

顯示: