Страницы

пятница, 27 января 2012 г.

Ловим события WMI. Повышаем "живучесть" процесса.

В этой статье я расскажу как ловить события, происходящие в операционной системе Windows, используя временную подписку WMI. Как известно, у временной подписки есть ряд недостатков, поэтому в процессе изложения мы рассмотрим несколько, на мой взгляд, экзотических способов для их преодоления. Are you ready ?

Определимся с задачей: поймать события запуска стандартного блокнота Notepad, завершения его работы, и сделать запуск скрипта автоматическим, а процесс отлова как можно более "живучим".

Картина первая. Начнем с запуска. Ловим создание экземпляра и оповещаем об этом диалоговым окном.
Const ffProcName = "notepad.exe"
'ловим событие создания процесса
Set colEvents = GetObject("winmgmts:\\.\Root\CIMV2").ExecNotificationQuery( _
  "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
  "AND TargetInstance.Name = '" & ffProcName& "'")  
Do 
 Set objEvent = colEvents.NextEvent  
 MsgBox "Вы запустили процесс " & objEvent.TargetInstance.Name
Loop
Сохраним файл с именем CatchNPStart.vbs. Запускаем скрипт, блокнот... Все по плану. Запускаем диспетчер задач, убиваем процесс wscript.exe.

Картина вторая. Усложняем задачу. Добавим к запуску событие завершения работы блокнота. Ловим события, выбираем создание либо удаление экземпляра, оповещаем в зависимости от события.
Const ffProcName = "notepad.exe"
'ловим события
Set colEvents = GetObject("winmgmts:\\.\Root\CIMV2").ExecNotificationQuery( _
  "SELECT * FROM __InstanceOperationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
  "AND TargetInstance.Name = '" & ffProcName & "'")
Do 
 With colEvents.NextEvent
  'если создание
  If .Path_.Class = "__InstanceCreationEvent" Then
   MsgBox "Вы запустили процесс " & ffProcName
  'если удаление
  ElseIf .Path_.Class = "__InstanceDeletionEvent" Then
   MsgBox "Вы завершили процесс " & ffProcName
  End If
 End With
Loop
Назовем файл CatchNPStartNFin.vbs. Запускаем скрипт, блокнот... Дело сделано.

Недостатки налицо: скрипт необходимо запустить и процесс его выполнения можно прервать.

Для решения проблемы запуска процесса у меня есть четыре варианта, три из которых в этой статье мы подробно рассматривать не будем по причине их тривиальности:
- автозапуск - разделы реестра Run, RunOnce
- логон-скрипт - он же сценарий входа
- Winlogon - чуть более "навороченный", но уже успевший стать довольно популярным способ, который используют всевозможные локеры
- четвертый вариант рассмотрим чуть позже


Итак, будем считать, что наш скрипт запущен при входе пользователя в систему, и теперь юзеру ничего не мешает запустить диспетчер задач Windows, Process Explorer от Марка Руссиновича (его имя мы еще вспомним далее в статье), или любой иной программный продукт, способный работать с процессами, и благополучно убить наше "творение" (что идет вразрез с поставленной выше задачей). Ограничения типа запрета использования диспетчера задач и пр. в расчет не берем. Пчелы начали что-то подозревать и они добьются своего :). Оградить свое времяпрепровождение от посторонних глаз в лице нашего скрипта для пчел - святое дело :).

Картина третья. Попробуем увеличить "плавучесть" процесса путем создания "катамарана" - напишем второй скрипт, задача которого состоит в том, чтобы следить за убийством процесса первого и возрождать его в случае необходимости.
Const wsProcName = "wscript.exe"
Const scrFileName = "CatchNPStartNFin2.vbs"

Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")
'ловим событие завершения скрипта
Set colEvents = objWMI.ExecNotificationQuery( _
  "SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
  "AND TargetInstance.Name = '" & wsProcName & "' AND TargetInstance.CommandLine LIKE '%" & scrFileName & "%'")  
Do 
 Set objEvent = colEvents.NextEvent 
 With CreateObject("Scripting.FileSystemObject")
  'перезапускаем скрипт
  objWMI.Get("Win32_Process").Create(wsProcName & " " & _
    Chr(34) & .BuildPath(.GetParentFolderName(.GetFile(WScript.ScriptFullName)),scrFileName)) & Chr(34)
 End With 
Loop
Сохраним файл с именем CatchScriptKill.vbs

Картина четвертая. Изменим код скрипта CatchNPStartNFin.vbs с тем, чтобы он в свою очередь следил за убийством скрипта CatchScriptKill.vbs - в противном случае наш "катамаран" далеко не уплывет.
Const ffProcName = "notepad.exe"
Const wsProcName = "wscript.exe"
Const scrFileName = "CatchScriptKill.vbs"

Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")
'ловим событие, добавим скрипт
Set colEvents = objWMI.ExecNotificationQuery( _
  "SELECT * FROM __InstanceOperationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
  "AND (TargetInstance.Name = '" & ffProcName & "' OR (TargetInstance.Name = '" & wsProcName & _
  "' AND TargetInstance.CommandLine LIKE '%" & scrFileName & "%'))")  
Do 
 With colEvents.NextEvent
  'если создание
  If .Path_.Class = "__InstanceCreationEvent" Then
   If .TargetInstance.Name = ffProcName Then
    MsgBox "Вы запустили процесс " & ffProcName
   End If
  'если удаление
  ElseIf .Path_.Class = "__InstanceDeletionEvent" Then
   'если огнелис
   If .TargetInstance.Name = ffProcName Then
    MsgBox "Вы завершили процесс " & ffProcName
   'если скрипт
   ElseIf .TargetInstance.Name = wsProcName Then   
    With CreateObject("Scripting.FileSystemObject")
     'перезапускаем скрипт
     objWMI.Get("Win32_Process").Create(wsProcName & " " & _
       Chr(34) & .BuildPath(.GetParentFolderName(.GetFile(WScript.ScriptFullName)),scrFileName)) & Chr(34)
    End With
   End If  
  End If
 End With
Loop
Назовем файл CatchNPStartNFin2.vbs. Не забудем убить запущенный ранее процесс wscript.exe. Запускам оба только что написанных скрипта (в любой последовательности), блокнот... Работает. Теперь попробуем убить процесс с помощью какого-нибудь диспетчера задач (я пробовал на обозначенных выше стандартном и Process Explorer), а он как феникс... Ну в общем понятно.

Картина пятая. С целью избавления системы от нашего "катамарана" пишем своего "киллера".
For Each objProcess In _
  GetObject("winmgmts:\\.\Root\CIMV2").ExecQuery("SELECT * FROM Win32_Process WHERE Name='wscript.exe'") 
 objProcess.Terminate()
Next
Назовем его WScriptKiller.vbs.
Можно обойтись и батником в одну строку.
taskkill /im wscript.exe /f
Картина шестая. Рассмотрим пропущенный выше четвертый вариант и вспомним Марка Руссиновича. Среди его разработок есть интересное приложение по имени srvany.exe, использование которого позволит нам запускать скрипт в качестве службы. Не совсем "наш", но зато наиболее эффективный метод: автозапуск + отсутствие процесса в каком-либо диспетчере задач.
Const scrFileName = "CatchNPStartNFin.vbs"
Const ServiceName = "MyService"
Const HKLM = &H80000002 

With CreateObject("Scripting.FileSystemObject")
 sPathName = .BuildPath(.GetParentFolderName(.GetFile(WScript.ScriptFullName)),"srvany.exe") 
 sParamName = "wscript.exe " &.BuildPath(.GetParentFolderName(.GetFile(WScript.ScriptFullName)),scrFileName)
End With

Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")  
Set objInstance = objWMI.Get("Win32_Service")
Set objMethod = objInstance.Methods_("Create")
Set objInParam = objMethod.inParameters.SpawnInstance_()

'задаем параметры
objInParam.Name = ServiceName
objInParam.DisplayName = "My Interactive Service"
objInParam.StartMode = "Automatic"
objInParam.DesktopInteract = True
objInParam.PathName = sPathName
'создаем службу
Set objOutParam = objInstance.ExecMethod_("Create", objInParam)

If objOutParam.ReturnValue <> 0 Then
 MsgBox "Не удалось создать службу " & ServiceName
 WScript.Quit
End If
 
'создаем ключи реестра, чтобы запускать srvany.exe с параметром - нашим скриптом
sKeyPath = "SYSTEM\CurrentControlSet\Services\" & ServiceName & "\Parameters"
With GetObject("winmgmts:\\.\root\default:StdRegProv")
 .CreateKey HKLM,sKeyPath
 .SetStringValue HKLM,sKeyPath,"Application",sParamName
End With

If Err.Number <> 0 Then
 MsgBox "Не удалось создать ключи реестра " & ServiceName
 WScript.Quit
End If

'запускаем службу
Set colServices = objWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & ServiceName & "'")
If colServices.Count Then
 For Each objService In colServices
  ret = objService.StartService
  If ret <> 0 Then
   MsgBox "Не удалось запустить службу " & ServiceName
   WScript.Quit
  End If
 Next
Else
 MsgBox "Не удалось создать службу " & ServiceName
 WScript.Quit
End If

MsgBox "Служба " & ServiceName & " установлена и запущена"
Сохраняем файл с именем ServiceCreate.vbs. Запускаем... Проверяем на нашем блокноте...

Картина седьмая. Не мешало бы научиться удалять наши службы.
Const ServiceName = "MyService"
Const HKLM = &H80000002

Set objWMI = GetObject("winmgmts:\\.\Root\CIMV2")

Set colServices = objWMI.ExecQuery("SELECT * FROM Win32_Service WHERE Name='" & ServiceName & "'")
If colServices.Count Then
 For Each objService In colServices
  ret = objService.StopService
  If ret <> 0 Then
   MsgBox "Не удалось остановить службу " & ServiceName
   WScript.Quit
  End If
  ret = objService.Delete
  If ret <> 0 Then
   MsgBox "Не удалось удалить службу " & ServiceName
   WScript.Quit
  End If
 Next
Else
 MsgBox "Служба " & ServiceName & " не найдена"
 WScript.Quit
End If

MsgBox "Служба " & ServiceName & " остановлена и удалена"
Назовем файл ServiceDelete.vbs. Удаляем нашу первую, очень функциональную, и для кого-то далеко не последнюю службу :).

Картина восьмая. Заключительная.
Содержание статьи - мой взгляд на временную подписку WMI и повышение "живучести" процесса. Для тех, кого заинтересовали технологии, использованные автором, хочу заметить, что в процессе изучения предметной области Вы сможете открыть для себя более "неубиваемые" способы мониторинга событий.
Удачи :).


p.s. Представьте насколько глубоко можно засадить в систему код, используя пару служб типа "катамарана" ;)?