Controlling pest-ware with asynchronous event monitoring

By The Microsoft Scripting Guys

Doctor Scripto at work

Doctor Scripto's Script Shop welds simple scripting examples together into more complex scripts to solve practical system administration scripting problems, often from the experiences of our readers. His contraptions aren't comprehensive or bullet-proof. But they do show how to build effective scripts from reusable code modules, handle errors and return codes, get input and output from different sources, run against multiple machines, and do other things you might want to do in your production scripts.

We hope find these columns and scripts useful – please let us know what you think of them. We'd also like to hear about other solutions to these problems that you've come up with and topics you'd like to see covered here in the future.

For an archive of previous columns, see the Doctor Scripto's Script Shop archive.

(Note: Many thanks Alain Lissoir, Chris Scoville, Mary Gray, and Steve Lee for their helpful suggestions.)

On This Page

Monitoring processes and services on multiple machines with asynchronous methods
WMI asynchronous methods
Checking what processes are running
Finding and terminating processes
Using async event handling to trap processes
Terminating and then monitoring target processes
Stopping and disabling services
Using asynchronous event handling to monitor services
Protecting against unwanted processes and services
Postscript

Monitoring processes and services on multiple machines with asynchronous methods

Times are good for viruses, worms, adware and spyware. They're such celebrities they've got their picture on the cover of the Rolling Stone – OK, slight exaggeration, it's actually in the New York Times and it's a picture of a doctor so besieged by adware that she no longer clicks on anything that pops up. The Times also interviewed an Internet-industry executive who threw his infested computer in the garbage because he felt it was cheaper and faster to buy a new one than to clean it up.

No, this is not an ad for the Scripting Guys virus-writing school. Hacking may seem cool to certain bored 16-year-olds, but it does a huge amount of damage to the industry in which we make our living, not to mention everybody else who uses computers.

Let's start by belaboring the obvious (a Scripting Guys specialty): there's no substitute for a comprehensive security strategy integrating anti-virus and anti-spyware applications. Once you have that in place, though, scripts can still be useful in related mop-up operations and other support tasks that have to be customized or automated quickly. What do you do before your anti-virus vendor can get you an update with a signature for a hot new worm? How about problems created by other sources—such as a disgruntled insider—that commercial software doesn't address? And what if the problem is merely irritating rather than sinister, like a popular game sucking up too much bandwidth on the corporate network?

In these sorts of situations, wouldn't it be handy if we could quickly whip up a script to monitor for symptoms of unwanted software, and if found, neutralize them?

Note: For the official guidance on how best to deal with security threats, refer to some of these resources:

We're not the only ones who have been thinking along these lines: Sergio, a system administrator for a community college system in southern California, suggested this idea:

"How about a script that finds bad services – connected with viruses, worms or spyware? Get the names of services from a list in a text file that can be maintained separately. Do this on multiple machines across the domain."

And Chad from Indiana sent us a script that looks for unknown processes on his machines.

Taking this issue to heart, Doctor Scripto has become a sort of pest-ware paparazzo: he likes to track down misbehaving bits of code and find out all their embarrassing secrets. But then instead of photographing them, he shuts them down and locks them out (and you thought he was just a harmless little virtual Scripting Guy).

To do his dirty work, the Doctor uses some scripting capabilities that we touched on in the last column. That piece discussed how to run processes and then track their duration, as you might want to do with a patch, upgrade, installation program or other utility. We're not going to talk about creating processes this time, but we are going to delve into a different way to monitor events that works particularly well when you have to do it on many machines. It's called asynchronous event monitoring.

We're going to use asynchronous event monitoring to trap specific unwanted processes and services, and then stop or disable them. The principles illustrated in these scripts can also be adapted to sniff out and handle other kinds of symptoms that unwelcome code guests might generate.

WMI asynchronous methods

In Doctor Scripto's last column, he called on the ExecNotificationQuery method of the WMI Scripting API's SWbemServices object to track processes that our scripts executed.

We ran into a problem with this method, however, when we wanted to monitor events on multiple machines. WMI methods can be synchronous, semisynchronous or asynchronous. ExecNotificationQuery is a synchronous or semisynchronous (the default for VBScript) event monitoring method. Synchronous or semisynchronous method execution means in this case that the SWbemEventSource object returned by ExecNotificationQuery for the first machine hangs around waiting for events on that machine to trigger its NextEvent method, and doesn't let the script move on to monitor events on other machines.

We solved the problem by spinning off a new script for each machine on the list, passing it the machine name and process ID of the process to monitor. That was not a bad solution, but the need to use multiple scripts gave it the faint aura of a Rube Goldberg machine.

Undaunted, Doctor Scripto dug deep into the entrails of WMI and came up with a less used but more elegant approach to event monitoring on multiple machines. And as we've often said, for the Scripting Guys elegance isn't everything, it's the only thing.

Asynchronous methods are a bit like a nature show on television where the photographer goes into the wild and plants a hidden camera with trip-wire triggers to record the comings and goings of the local wildlife. The photographer doesn't have to be there to snap the shutter because the trip-wires do that for him and the camera retains the images. In this column, though, the elusive critters we'll be stalking, rather than baboons or oryxes, will be questionable processes and services.

The SWbemServices object, one of the workhorses of the WMI Scripting API and most of our sample scripts, has several asynchronous methods that are the patient counterparts of their synchronous or semisynchronous siblings: ExecQuery has ExecQueryAsync, InstancesOf has InstancesOfAsync, and to the Doctor's delight, ExecNotificationQuery has ExecNotificationQueryAsync.

Where the synchronous or semisynchronous methods take a more direct approach, performing an action and then pausing to find out what happened before returning a result, the asynchronous methods make their query and return on the double. But they leave behind something called a sink, a special kind of object that waits around to receive the results of the asynchronous methods. The WMI Scripting API includes a special type of object, SWbemSink, that performs this function for asynchronous methods.

Think of the sink as that remote camera and sensors that the method leaves behind to report on the ongoing activities it wants to investigate.

In the case of the ExecNotificationQueryAsync method, the results are events, and the script that calls this method becomes what WMI refers to as an event consumer. The SWbemSink object serves here as an event sink that waits on the target machines for the kinds of events specified in the query to occur. When such an event comes along, it triggers an OnObjectReady event of SWbemSink, which the script catches and handles. (You can find more complete information on asynchronous event monitoring in the WMI SDK.)

Running ExecNotificationQueryAsync

Sinks waiting for OnObjectReady

They Suspended My Poetic License Department:

Actually, you can monitor all three remote machines with one SWbemSink object, and it is instanciated on the local machine where the script is running.

A big advantage of using the ExecNotificationQueryAsync method for monitoring events on multiple machines is that it can use event sinks to monitor those machines within the same script, rather than having to kick off separate scripts for each machine.

We discussed the basics of event queries in the column before last, "It's 2 a.m. Do you know where your processes are?" Incidentally, if you're new to WMI events, check out the Scripting Guys webcast, An Ounce of Prevention: An Introduction to WMI Events.

We're going to take advantage of the benefits of asynchronous event monitoring later in this column. But for now, let's review the simpler, more familiar scripting tasks involved in Dr. Scripto's monitoring extravaganza.

Checking what processes are running

If we're concerned that certain undesirable processes may be running on a machine, we can check for them first with a variant on one of the most basic scripts. If you've attended Scripting Guys webcasts or workshops or read the Windows 2000 Scripting Guide, you've probably encountered one of our simple scripts that lists the processes running on a computer.

This script goes one step further: it checks those running processes against a list of process names and displays those that match. As proxies for delinquent processes, we're using Calculator, Notepad and Freecell, so to test this script, open a couple of windows of each of these applications.

Listing 1: Find processes.

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

We put the list of processes we want to check on in a VBScript array, which works fine for a small number of items. We query WMI for a collection of running processes, and then we use nested For Each loops to compare the processes in the list with the processes running on the machine.

The first For Each statement loops through the collection of processes that WMI returns. For each of those processes, the inner For Each statement loops through the processes in the array and checks each against the current running process. We use the built-in VBScript LCase function to make all the process names lower-case so that we don't miss a comparison because one name uses different capitalization. When we find a match, we display the name of the process – remember that there can be more than one process with the same name running for many executables.

OK, now that we've identified the processes we don't want loitering on our machine, what are we going to do with them?

Finding and terminating processes

Conveniently, the WMI class Win32_Process offers us a method called Terminate, which does pretty much what you'd expect. First you get a reference to the process object that you want to terminate, then you call the Terminate method on that object. In a sense, you're asking that process to self-destruct.

objProcess.Terminate

This euphemism contrasts with the less genteel naming of the command-line tools that do the same thing: Kill.exe and Taskkill.exe (post-Windows 2000).

So you could look at the next script as a kind of an action movie in which the hero, Dr. Scripto, moves through a landscape of potentially hostile processes, searching and destroying. Or if you'd prefer a less bellicose metaphor, how about a peaceful garden in which he roots out weedy processes and tosses them onto the compost heap to plunge into the great carbon cycle once again? No problem at all, gentle reader, the Scripting Guys aim to please.

As with Listing 1, before running the script, open as many instances of the target processes as suits your fancy and watch them close when you run it.

Listing 2: Terminate processes.

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

Listing 2 is pretty much identical to Listing 1 up through the comparison of running processes against the names on the list in the array. But now, in addition to echoing the names of the matching processes, we call the Terminate method on those processes as well. Then we check the value that Terminate returns: as with nearly all WMI methods, if the value is 0 that means that the process was successfully terminated, so we display a message to that effect. Any other value indicates some kind of problem which prevented the method from executing successfully (values are listed in the WMI SDK topic).

Using async event handling to trap processes

If you think about it, all the second script did was to stop the process – it didn't delete the executable that ran the process. Couldn't Freecell junkies simply start up their game again? And what if a virus used some devious mechanism to restart itself?

You could delete the executable, but in the case of a game that might amount to excessive force. In other situations, too, you might not want to remove the file. There are many possible actions you could script to deal with these sorts of situations, and as much as Doctor Scripto would love to cover them all in this column, we don't want it to become his first novel. So let's focus on one way we can prevent those unwanted processes from starting up again.

In action-movie terms, let's terminate those processes with extreme prejudice. Doctor Scripto tried to come up with a comparable analogy for weeds in the garden, but the well was dry.

Here's where the asynchronous monitoring we talked about earlier comes in. Listing 3 monitors the same processes we shut down in Listing 2 and, if they start up again, terminates them again. Look at it as integrated pest management for operating systems.

To take this script for a test drive, when the message "Waiting for target processes ..." appears, try opening the target apps. The script should close them almost immediately. Seriously, have you ever had this much fun with a script?

Listing 3: Monitor and terminate processes asynchronously.

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

Notice that in the WQL query passed to ExecNotificationQueryAsync, the script uses WITHIN 1 to define the polling interval. This means that the query will poll every second. For our simple demo, this works fine. But to run a script like this on a large number of machines or with a query that returns large amounts of data, you should experiment with a larger number for the polling interval. WITHIN 10 would consume less processing bandwith, and would create less network activity if run on a remote machine. Of course, the downside is that it would also allow more time for unwanted processes to run before they were terminated. The best setting for a specific environment will need to be worked out and tested in that context.

Asynchronous methods require that the script create a sink, which we discussed earlier. To do this, we call the Windows Script Host method CreateObject and pass it two parameters: the programmatic ID WbemScripting.SWbemSink and the identifier (in this case, SINK_), that we will append to the beginning of SWbemSink methods such as OnObjectReady when we call them later in the script.

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

The sink that we created has events associated with it, the main one of interest to us here being OnObjectReady. OnObjectReady is triggered whenever an object provided by the asynchronous call is available to the sink, which in this case means whenever the creation of a process occurs. To handle those event objects, we have to write a subroutine that begins with the prefix we specified in the CreateObject call (SINK_) and ends with the name of the event. So our sub that traps events is called SINK_OnObjectReady.

When we connect to WMI and call the ExecNotificationQueryAsync method, we pass it two parameters:

  • The name of the sink object we just created, in this case SINK (no points for originality here).

  • The WQL event query to execute, in this case "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'"

In this case, we're asking WMI to notify us of any event in which a process was created, checking every second. After that, the script goes into an endless loop waiting for events to occur. To end the loop, press Ctrl+C.

When an event that matches the criteria in the query fires, SINK_OnObjectReady returns an out parameter which we call objLatestEvent, an object representing the new event (we don't use objAsyncContext, the other parameter, in this script). As with the synchronous and semisynchronous queries we discussed in the previous two columns, the TargetInstance property of objLatestEvent gives the script access to all the properties of the instance of the object that triggered the event, in this case a Win32_Process object. So we can use objLatestEvent.TargetInstance.Name to find out the name of the process. Better yet, TargetInstance also lets us call methods on the instance.

Now, with the name of the process that triggered the event, we loop through the list of target processes and check to see if any of them match. If it does, we call the Terminate method on the instance object:

intReturn = objLatestEvent.TargetInstance.Terminate

Before the process can get up and running, it's down and out again. Not a very fair fight.

Terminating and then monitoring target processes

Now let's put the scripts from Listings 2 and 3 together, terminating any processes specified in the list and then asynchronously monitoring for these processes to make sure they don't start up again. We also want the script to run against multiple computers.

To test the script, substitute the names of computers in arrComputers with computers accessible on your network on which you have Administrator privileges. Open several instances of the target processes, and once the script has closed them, try to open them again.

Listing 4: Terminate target processes and then monitor for new ones.

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

In this script, we put the code that initially terminates running processes into a function called KillProcesses, and the code that monitors process events into a sub called TrapProcesses. We also isolate the code that handles errors into a sub called – you guessed it – HandleError, so that we don't have to repeat it in several different places. As you can see, we're trying to give procedures clear, simple names that describe what they do in a verb-noun format. As you might imagine, we have to fight Doctor Scripto every step of the way on this: he prefers names like "OffTheProcess," "FunkyFunkyAsync," and "DoTheErrNow."

In the main logic section at the top of the script, we create an event sink. As in the previous script, we also create a sub (further down in the script) that handles the OnObjectReady event of the sink.

Note that we've renamed the array of target processes "g_arrTargetProcs". The "g_" prefix is our convention to indicate a global variable, one that is available to all of the subs and functions as well as the main body of the script. Using a global variable for this saves us the trouble of passing the process names to each procedure separately.

We then loop through the list of computers. On each computer, we begin by calling KillProcesses, which returns a 1 if it's unable to connect to WMI on the remote machine. In that case, the script does not try to monitor events, but moves on to the next machine.

If KillProcesses has been able at least to connect to the machine, it checks for the target processes. If it finds one, it tries to terminate it. It keeps track of the target processes found, terminated, and not terminated, and displays a message with these counts.

As long as KillProcesses has connected to the machine, even if it has failed to terminate some processes, the main logic goes ahead and calls the TrapProcesses sub, which runs an asynchronous event query on that machine. It forges ahead because even if the first pass failed to terminate some processes, it's worth it to try to monitor for new ones. And of course, if KillProcesses found no target processes, the script should still continue to monitor for them in case one starts up.

If successful, the asynchronous event query connects the event monitoring on that machine to the central event sink that was created at the beginning of the script. Although event monitoring on all the machines feeds back into the same sink, the sink can distinguish which machine the event occurred on by checking the CSName (computer system name) property of Win32_Process for that event.

As in the previous script, if TrapProcesses detects a targeted process, it immediately calls Terminate and attempts to stop it. If it succeeds, it does the FunkyFunkyAsync.

Stopping and disabling services

Now we've got a simple working script to deal with processes, but we still haven't said anything about services. Many applications run in the form of a service instead of or in addition to a process. How do we take care of unwanted services as we did with processes?

Our solutions for services are pretty similar to the process scripts. Our goal is to disable the undesired services so that they won't start up again, but here we run into a complication: if we disable a service while it's running, that still doesn't stop the service. It only changes the startup type for the future. So our script needs to both stop the service and disable it so it can't start up again.

If you try this script, be sure to note the Status and Startup Type of each service you test on before running the script, and return them to their original condition when you're done. This example uses the operating system services Alerter, Smart Card and Wireless Zero Configuration as test targets; you may substitute other ones if you prefer. Note that in arrTargetSvcs you have to use the service name rather than the display name (as the terms are used in Services.msc).

Listing 5: Stop and disable services.

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

The query for ExecQuery here is very similar to the one we used in Listing 1, except that here we use the Win32_Service class rather than Win32_Process. The method returns a collection of all the services installed on the machine.

Services' Status and Startup Type (names used by the Services snap-in) are independent: if a service is disabled while it's running it will continue to run. Conversely, a service can be stopped but not be disabled. So it's necessary to both stop these services and disable them.

To perform these two tasks, the script first checks whether the service is running. Win32_Service uses the State property for what the snap-in calls Status, so the script checks the State property. If the value is not "Stopped" the script calls the StopService method.

Then the script checks the StartMode property, which the snap-in calls Startup Type. If this is not "Disabled" the script calls the ChangeStartMode method, passing it "Disabled" as the only parameter.

When the script finishes running, all the target services should be stopped and disabled.

Using asynchronous event handling to monitor services

As with processes, we can't count on malicious services staying stopped and disabled. So the following script illustrates how to use asynchronous event queries to keep an eye on services on the list to make sure they don't start up again.

This script runs continuously. To end it, press Ctrl+C.

Listing 6: Monitor, stop and disable services asynchronously.

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

Once again, this script closely resembles Listing 3, the process monitoring and terminating script. The differences parallel those in Listing 5, the script that stops and disables services.

The query passed to ExecNotificationQueryAsync in this case filters for cases where TargetInstance is a Win32_Service rather than a Win32_Process. It also selects instances of __InstanceModificationEvent rather than of __InstanceCreationEvent, which the script in Listing 3 did. That's because all the services in question have already been installed: the events of changing start mode and starting up are modifications, not creations, of instances.

When a target event is trapped, the SINK_OnObjectReady sub must both stop the service and disable it. It accomplishes these tasks by calling two methods of Win32_Service, StopService and ChangeStartMode, and passing the latter the parameter "Disabled".

Protecting against unwanted processes and services

Now that we have reviewed the chunks of code necessary to accomplish each task, it's time for Doctor Script to fire up his welding torch and fit all the pieces together into one omnibus script that pulls all the previous scripts in this column together.

Our final script:

  • Gets a list of processes from a text file.

  • Gets a list of services from a text file.

  • Gets a list of machines from a text file.

  • On each machine:

    • Checks for processes on the list.

    • If any are found, terminates them.

    • Check for services on the list.

    • If any are found, stops and disables them.

    • Spins off an async event sink to monitor __InstanceCreationEvent for all processes on the list.

    • If any of these processes start, terminates them.

    • Spins off an async event sink to monitor __InstanceModificationEvent for all services on the list.

    • If any of these services start, stops and disables them.

  • Logs results to a text log file and displays them in the command-shell window.

Script logic - final script

As always in our columns, this script is an extended template for you to work with. You will need to customize it to make it work in your environment and you will probably want to improve on it and add to it as well. We have tested it on small groups of machines running Windows XP and Windows Server 2003, but it has not been tested exhaustively against many machines or in more heterogeneous environments.

Here are examples of the text files for input. All should be in the same directory as the script. You can change the filenames and paths in the change block at the beginning of the script.

The following file is named complist.txt in the example script. Change the computer names to those of machines accessible on your network and on which you have Administrator privileges.

Listing 7a: Text file with list of computers.

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

The following file is named proclist.txt in the example script. Substitute the process names of processes you want to monitor.

Listing 7b: Text file with list of processes.

calc.exe
notepad.exe
freecell.exe

The following file is named svclist.txt in the example script. Substitute the service names (rather than the display names) of services you want to monitor.

Listing 7c: Text file with list of services.

Alerter
SCardSvr
WZCSVC

To test this script, open several instances of the target processes and check the status of the target services to ensure that they are not stopped and disabled. Once the script has terminated the processes and stopped and disabled the services, try to run the processes and change the startup type of the services to manual or automatic (you cannot start a service while it is disabled). The script should stop the processes and change the startup type of the services back to disabled.

This script runs continuously. As with any script or executable running in a cmd.exe window, press Ctrl+C to end it.

Listing 7: Terminate target processes, stop and disable target services, and monitor all to prevent recurrences.

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

Notice that at the beginning of the script, all the variables that might need to be changed to run the script in a different environment are grouped in a section labeled "Change block." This makes it easier to keep track of current values of the variables.

Another different approach this script takes from the previous ones is to create two separate event sinks, one for process events and one for service events, with this code:

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

This also requires two separate subs to handle the different events, SINK1_OnObjectReady and SINK2_OnObjectReady. While it is possible to collect all events in one sync, the work of then distinguishing between the event sources made this approach conceptually simpler to code.

The main logic in the body of this script is not very different from that in Listing 4: Terminate target processes and then monitor for new ones. Here, though, the script is monitoring both processes and services, so it creates two parallel logic paths which are independent of each other.

The script first loops through the list of machines and attempts to bind to WMI on each machine. This serves as a check on network connectivity, as well as ascertaining that the WMI service is running on the machine.

For faster performance against large numbers of machines, you could add a function that pings each machine first. But WMI returns an error within a few seconds if it is unable to bind to the machine, and is adequate for our purposes here.

Then, on the current machine, the script calls KillProcesses and DisableServices in succession, both of which should run quite quickly. The logic checks the return value of KillProcesses first, then that of DisableServices. Each of these functions returns a 1 if ExecQuery threw an error on that machine, and 0 if ExecQuery ran successfully. Even if no target processes or services were found, or if some were found and not stopped, the logic goes on to call the respective monitoring function. It makes sense that the logic for processes is independent from the logic for services, because the script should go on to monitor both target processes and services regardless of whether any of either are found initially.

As long as KillProcesses and DisableServices are able to query the current machine, TrapProcesses and TrapServices are then called. These subs each run an asynchronous query and spin off their own event sinks to catch their respective events.

You've already seen all of the other procedures used in Listing 7. KillProcesses, DisableServices, TrapProcesses, and TrapServices have been explained above. ReadTextFile, WriteTextFile and HandleError have been used in previous columns.

The way this script uses the ReadTextFile function illustrates one reason for breaking scripts into procedures. ReadTextFile enables the script to open each of the three input files with the same function, which reads each file out into an array and returns the array. The arrays of computers, processes and services retrieved this way are then used by other code. If you didn't encapsulate the code for reading text files in a function, you would have to repeat it three times in the script body.

Because the script outputs results to both the command-shell window and a text log file, all the procedures write results to a string, appending new information as they go. For example:

strPData = strPData & vbCrLf & "    Terminated"

appends a string containing white space for indentation and "Terminated" onto whatever was already in strPData, in this case a description of a process.

At the end of each procedure, lines such as:

WScript.Echo strPData
  WriteTextFile g_strOutputFile, strPData

display the finished string at the command prompt and call WriteTextFile to append that string, strPData, to the text log file. To enable this cumulative collection of output, WriteTextFile opens the log file for appending with this code:

Set objTextStream = objFSO.OpenTextFile(strFileName, FOR_APPENDING)

This means that each successive string written to the log file is added onto the existing contents rather than overwriting them.

Here is a hypothetical log file:

Listing 8: Text file output log.

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

We could extend this script to perform tasks such as deleting files or registry entries and removing users or shares. The modular construction of Listing 7 makes it relatively easy to add functions to it that perform these tasks. But Dr. Scripto is once again asleep on his desk, drained by the exertion, and if you have come this far, patient reader, perhaps you are ready for a break, too.