Skip to content

windows

Process Killer

Quando un programma va in crash la colpa viene attribuita sempre al programmatore. Mentre quando un programma si blocca, o è troppo lento, è di certo per qualche misfatto dell'amministratore di sistema. Anche se magari il programma è scritto con i piedi ed è propenso all'inchiappettamento di tutti i server su cui gira.

Per ristabilire un po' di giustizia divina, ho pensato di killare senza pietà tutti i processi che utilizzano ferocemente la CPU per più di un determinato tempo. Metodo barbaro, ma molto in voga negli Unix dei vecchi tempi... e non solo.

Naturalmente nel mio caso si tratta di processi che dovrebbero essere molto leggeri ma per qualche strana ragione si incastrano e finiscono per utilizzare il processore a pieno regime, infastidendo anche tutti gli utenti di programmi buoni e gentili.

Come punto di partenza, direi di dare un'occhiata all'output del comando pslist , che lista tutti i processi in esecuzione su di una macchina (locale o remota, a patto di avere i privilegi necessari):

pslist v1.29 - Sysinternals PsList
Copyright (C) 2000-2009 Mark Russinovich
Sysinternals

Process information for ER-MEJO:

Name                Pid Pri Thd  Hnd   Priv        CPU Time    Elapsed Time 
Idle                  0   0   2    0      0    23:05:50.623     0:00:00.000
System                4   8 114 4338     52     0:11:32.114    28:42:03.487
smss                252  11   3   30    340     0:00:00.171    28:42:03.487
csrss               348  13   9 1033   2744     0:00:06.645    28:41:51.132
wininit             416  13   3   79    992     0:00:00.093    28:41:50.414
explorer           4448   8  36 1252  69768     0:07:39.797    28:41:04.882
iexplore           3772   8  13  382   7172     0:00:04.539     3:14:05.766
iexplore           5788   8  22  704  97632     0:05:51.345     3:14:02.648
PsList             4136  13   1  151   1980     0:00:00.187     0:00:00.172

le cui colonne interessanti sono le prime due (nome del process e PID) e le ultime due:

Elapsed Time: tempo totale di esecuzione del processo
CPU Time: tempo effettivamente utilizzato dal processo

ad esempio explorer è in esecuzione da 28 ore e 41 minuti, e durante questo periodo ha utilizzato la cpu per 7 minuti e 39 secondi. Proviamo a rilanciare il psexec e a vedere che cosa succede:

Name                Pid Pri Thd  Hnd   Priv        CPU Time    Elapsed Time 
Idle                  0   0   2    0      0    23:06:42.540     0:00:00.000
System                4   8 114 4342     52     0:11:32.426    28:42:33.493
smss                252  11   3   30    340     0:00:00.171    28:42:33.493
csrss               348  13   9 1050   2744     0:00:06.645    28:42:21.138
wininit             416  13   3   79    992     0:00:00.093    28:42:20.420
explorer           4448   8  42 1295  71484     0:07:40.234    28:41:34.888
iexplore           3772   8  13  389   7600     0:00:04.617     3:14:35.772
iexplore           5788   8  23  716  95520     0:05:53.513     3:14:32.654
PsList             1792  13   1  151   1988     0:00:00.171     0:00:00.156

il comando è stato lanciato circa 30 secondi dopo il primo, ed infatti tutto gli Elapsed Time sono aumentati circa di 30 secondi, processo PsList escluso ma se osserviamo bene si tratta di un nuovo processo con un nuovo PID.

Anche la colonna CPU Time è variata per alcuni processi, e se dividiamo il delta del CPU Time per il delta dell'Elapsed Time possiamo ottenere... la percentuale di utilizzo del processore di questo processo relativamente la periodo di tempo intercorso tra i due lanci del comando psexec.

Da notare che se la macchina dispone di più processori nel Task Manager di Windows vedremo la percentuale riferita a tutte le CPU: un processo che gira su di un singolo processore non potrà quindi superare il 25% di CPU se la macchina dispone di 4 processori.

Con il comando psexec invece non ci dobbiamo preoccupare del numero di processori che ha la macchina: il tempo di CPU utilizzato diviso il tempo di CPU avuto è disposizione può variare da zero a uno ed è l'utilizzo effettivo del signolo processore.

Ho scritto questo codice in Python che controlla ad intervalli regolari un elenco di server, e che giustizia tutti i processi che durante questo periodo di tempo hanno consumato più del 90% della propria CPU:

from __future__ import division
import subprocess
import re
import time

word = dict()
pausa = 30
servers = ('citrix01', 'citrix02', 'citrix03', 'server', 'server-2')

def milliSeconds(time):
  tm = re.match("(\d*)\:(\d*)\:(\d*)\.(\d*)", time)
  if tm:
    return long(tm.group(1)) * 60*60*1000 + long(tm.group(2)) * 60*1000 + long(tm.group(3)) * 1000 + long(tm.group(4))
  else:
    return 0

def getProcess(server, nome):
  pslist = subprocess.Popen('"c:\program files\utils\pslist.exe" \\\\' + server + ' ' + nome, shell=False, stdout=subprocess.PIPE)

  for line in pslist.stdout:
    proc = re.match("^(\w+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([\w\:\.]+)\s+([\w\:\.]+)", line)
    if proc:
      serverPid = server + '.' + proc.group(2)
      if serverPid in word:
        if (milliSeconds(proc.group(7))-milliSeconds(word[serverPid][1]))/(milliSeconds(proc.group(8))-milliSeconds(word[serverPid][2]))>0.90:
          subprocess.Popen('"c:\program files\utils\pskill.exe" \\\\' + server + ' ' + word[serverPid][4])
      else:
        if proc.group(1) <> "Idle" and proc.group(1) <> "System":
          word[serverPid] = [proc.group(1), proc.group(7), proc.group(8), server, proc.group(2)]

def main():
  while True:
    for server in servers:
      getProcess(server, 'winword')
      time.sleep(pausa)

if __name__ == "__main__":
    main()

da sistemare per bene... e da usare con opportuna cautela!

Bloccare gli aggiornamenti software

Disabilitare gli aggiornamenti del sistema operativo oppure del software applicativo è in genere una pratica da evitare : un sistema aggiornato è più stabile, e molto più sicuro!

Ad oggi le poche aziende che non hanno attivato un sistema di aggiornamento automatico per il sistema operativo Windows ne pagano le conseguenze (vedi ad esempio Conficker). Ed è curioso notare che, poiché un sistema Windows aggiornato e configurato adeguatamente offre sistemi di sicurezza avanzati e difficili da aggirare, si sta assistendo ad un cambio di tendenza e l'obiettivo principale degli attacchi del malware più recente si sta spostando dal sistema operativo agli applicativi utente installati sopra.

Applicazioni che girano in modalità utente quali Flash Player, Silverlight, Acrobat Reader, Internet Explorer, Firefox e relativi plugin, sono tutte ottime candidate.... i bug non mancano mai, e se ci riflettiamo un poco perché attaccare tutto il sistema? Dati sensibili, password e quant'altro si trovano proprio dal lato utente!

Naturalmente verso questo tipo di attacchi il famigerato UAC di Windows Vista e Windows 7 spesso non può fare molto... così come può fare ben poco la richiesta di password dei vari Linux, o MacOS: il sistema è protetto, e va bene, ma i dati degli utenti forse non troppo!

Perché quindi bloccare gli aggiornamenti del software applicativo?

Ci devono essere delle ottime ragioni, ed una di queste è che gli aggiornamenti spesso e volentieri sono progettati per funzionare su computer domestici , in cui l'utente ha accesso ad Internet completo ed accede al desktop con amministrativi.

In un'azienda non tutti i PC hanno accesso ad Internet , quelli che vi accedono probabilmente dispongono di un accesso filtrato , ed anche se riescono a scaricare gli aggiornamenti, propabilmente non riescono ad installarli poiché l'utente non dispone di diritti per aggiornare il software installato.

Normalmente il software applicativo presente nei computer aziendali non è lo stesso software che può scaricare l'utente finale: in genere è preferibile distribuire versioni amministrative o aziendali oppure versioni ripacchettizzate (es. Firefox, Flash Player, e Acrobat Reader si trovano anche in versione MSI, e nel caso in cui non si trovino, basta cercare per esempio sul sito www.appdeploy.com per trovare tutto quello di cui c'è bisogno).

E normalmente il software applicativo viene configurato per non aggiornarsi automaticamente: gli aggiornamenti sono disabilitati, perché è inutile disporre di decine di updater uno per ogni servizio, quando invece sono disponibili applicazioni apposite per la distribuzione di tutti gli aggiornamenti!

Cosa fare per tutto il software che invece è stato scaricato da una versione "tradizionale" e quindi con gli aggiornamenti attivati?

In genere le informazioni di configurazione sono sparsi in qualche chiave di registro, naturalmente nella HKLM. Abbiamo bisogno quindi di un buon script WSH da eseguire all'avvio della macchina da lanciare con privilegi amministrativi.

Creiamo una nuova policy, e su Computer Configuration - Windows Settings - Scripts aggiungiamo uno script di avvio simile al seguente:

Dim WshShell
Set WshShell = WScript.CreateObject("WScript.Shell")

On Error Resume Next

' disabilita aggiornamenti Adobe Acrobat

WshShell.RegWrite "HKLM\Software\Policies\Adobe\Acrobat Reader\8.0\FeatureLockdown\bUpdater", 0, "REG_DWORD"
WshShell.RegWrite "HKLM\SOFTWARE\Policies\Adobe\Acrobat Reader\9.0\FeatureLockdown\bUpdater", 0, "REG_DWORD"
WshShell.RegWrite "HKLM\SOFTWARE\Adobe\Acrobat Reader\9.0\AdobeViewer\EULA", 1, "REG_DWORD"
WshShell.RegWrite "HKLM\SOFTWARE\Adobe\Acrobat Reader\9.0\AdobeViewer\Launched", 1, "REG_DWORD"

' disabilita aggiornamenti Java

WshShell.RegWrite "HKLM\SOFTWARE\JavaSoft\Java Update\Policy\EnableJavaUpdate",  0, "REG_DWORD"
WshShell.RegWrite "HKLM\SOFTWARE\JavaSoft\Java Update\Policy\EnableAutoUpdateCheck", 0, "REG_DWORD"

Wscript.Quit

Questi sono i parametri che ho trovato in giro. Ovviamente possiamo personalizzare questo script a piacimento per supportare altri programmi oppure altre versioni. Naturalmente cerchiamo di farne buon uso!

Llo stesso identico procedimento può essere utilizzato per modificare all'avvio qualsiasi chiave di sistema oppure, se creiamo lo script lato utente, possiamo impostare le chiavi in HKEY_CURRENT_USER.

Molto utile quindi non solo per gli aggiornamenti, ma anche per poter preconfigurare ad ogni avvio una buona parte del software lato desktop.

Chi è l'utente .Default?

Sfogliando la chiave di registro HKEY_USERS possiamo osservare che sono presenti tutte le chiavi HKEY_CURRENT_USER, organizzate per SID, di tutti gli utenti attualmente connessi al sistema.

La HKEY_CURRENT_USER di ogni utente si trova nel file NTUSER.DAT all'interno di ogni profilo. Quando un utente effettua l'accesso al computer, il sistema operativo carica il file NTUSER.DAT nella HKEY_USERS assieme a tutti gli altri utenti, ed ogni utente nella HKEY_CURRENT_USER vede soltanto le sue chiavi.

Oltre ai SID degli utenti, possiamo osservare che è presente un utente .Default. Ma chi è l'utente .Default?

Naturalmente è ragionevole pensare che l'utente .Default sia utilizzato come profilo template per i nuovi utenti: modificando le impostazioni dell'utente .Default è logico aspettarsi che tutti i nuovi utenti creati nel sistema ereditino queste impostazioni.

In effetti la scelta del nome .Default è stata infelice, e le cose vanno diversamente. Le chiavi di registro dell'utente .Default appartengono all'utente Local System, mentre il template di default per i nuovi utenti di sistema non è generalmente caricato in memoria e si trova nel percorso C:\Users\Default\NTUSER.DAT per Vista o Seven, oppure in C:\Documents and Settings\Default User\NTUSER.DAT per XP.

Per modificare le impostazioni di default è possibile caricare manualmente questo Hive in memoria, modificarlo e salvarlo, anche se questo metodo non è ufficialmente supportato.

Conviene utilizzare il buon SysPrep... oppure ho risolto il problema con un meraviglioso script WSH all'avvio.

Per approfondire: The .Default user is not the default user.

Variabili d'ambiente nello script di Logon

Come settare le variabili di ambiente in uno script di logon?
In VBScript si può fare così:

Set WshShell = WScript.CreateObject("WScript.Shell")
Set WshUEnv = WshShell.Environment("User")

WshUEnv("INIAPP") = "%USERPROFILE%\APP.INI"

(in questo esempio ho settato alla variabile INIAPP, relativa all'ambiente dell'utente, il valore "%USERPROFILE%\APP.INI"). Per rendere la modifica permanente, aggiungo anche la seguente riga:

WshShell.RegWrite "HKCU\Environment\INIAPP", "%USERPROFILE%\APP.INI", "REG_EXPAND_SZ"

Capita spesso inoltre di dover modificare il PATH di ricerca. La soluzione che utilizzo è la seguente, e mi permette di evitare di avere PATH doppi:

Set WshShell = WScript.CreateObject("WScript.Shell")
Set WshUEnv = WshShell.Environment("User")
Set pp = CreateObject("Scripting.Dictionary")

for each p in Split(WshUenv("PATH"), ";")
  AddPath p
next

AddPath "C:\PROGRAMMA1"
AddPath "C:\ORACLE"
AddPath "C:\TEST"

WshUEnv("PATH") = Join(pp.Keys, ";")
WshShell.RegWrite "HKCU\Environment\PATH", Join(pp.Keys, ";"), "REG_EXPAND_SZ"

pp.RemoveAll
Set pp = Nothing
Set WshShell = Nothing
Set WshUEnv = Nothing

Sub AddPath(path)
  If Not pp.Exists(path) And Trim(path) <> "" Then
    pp.Add path, ""
  End If
End Sub

Suddivido il percorso originario in un array con i percorsi singoli, e con l'oggetto Scripting.Dictionary aggiungo ogni singolo percorso, senza duplicati. Poi ricompongo la stringa.
Semplice ma elegante!

Eliminare File Temporanei

Spesso applicazioni ed installer lasciano in giro per l'hard disk un sacco di spazzatura, dimenticandosi allegramente di cancellarla. Anche se è vero che i dischi sono sempre più capienti, e che un po' di file in eccesso non fanno poi così male come molti credono, ogni tanto vale la pena mettere un po' di ordine e di fare un poco di pulizia.

Posizioni dei file temporanei
Dove vengono creati generalmente i file temporanei? In genere vengono salvati nelle seguenti posizioni:

  • %WINDIR%\Temp (tipicamente C:\WINDOWS\Temp)
  • %TEMP% (tipicamente C:\Users\nomeutente\AppData\Local\TEMP, in Windows Vista)

(i file temporanei di Internet Explorer o di altri browser preferisco non toccarli: in questo articolo voglio solo eliminare soltanto i file che si trovano nelle cartelle temporanee utilizzate dagli altri programmi).

Per spazzare via un po' di file inutili dovrebbero quindi essere sufficienti i seguenti comandi:

DEL /F /S /Q "%WINDIR%\Temp"
DEL /F /S /Q "%TEMP%"

Giusto? Al di là del fatto che questi comandi sono particolarmente delicati (attenzione a non sbagliare il percorso!) è importante notare che questi comandi non sono sufficienti in un ambiente multiuser!

Si noti infatti che %TEMP% contiene soltanto il percorso dei file temporanei che appartengono all'utente corrente, ma ogni utente di sistema dispone della sua personale cartella temporanea!

Per svolgere bene il nostro compito, dobbiamo quindi scorrere tutte le variabili di ambiente di tutti gli utenti presenti nel sistema!

Come conoscere tutti gli utenti?
L'elenco degli identificativi degli utenti (detto SID) che dispongono di un profilo nel sistema lo si ottiene interrogando la chiave HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\ProfileList:

REG QUERY "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\ProfileList

ed all'interno di ogni SID possiamo ottenere il percorso su disco del profilo interrogando la chiave ProfileImagePath.

Dove sono memorizzate le variabili d'ambiente?
Le variabili per l'utente corrente si trovano in HKEY_CURRENT_USER\Environment , mentre le vabili di tutti gli utenti attualmente attivi sul sistema le possiamo recuperare in HKEY_USERS\SID\Environment.

Se vogliamo fare le cose fatte bene dobbiamo scorrere le chiavi anche per tutti gli utenti che non sono attualmente attivi: in questo caso Windows non carica il profilo in memoria nella HKEY_USERS ma tutte le chiavi si trovano su disco nel file NTUSER.DAT che si trova all'interno di ogni ProfileImagePath.

Lo script
Nello script seguente ho mescolato un po' di tecniche diverse: WMI per scorrere il registro, WScript.Shell per leggere il registro in modalità non espansa (le variabili d'ambiente che contentono il percorso %USERPROFILE% escono tutte espanse con il profilo dell'utente che lancia lo script, e non va bene! Ovviamente non mi piace questo metodo, e spero di trovare una soluzione migliore), ed infine per caricare il file NTUSER.DAT in una chiave temporanea, non ho trovato nulla di meglio di lanciare dalla shell un buon REG LOAD...arh, brutto ma funzionale!
Fortuna che dopo qualche errore di codifica mi rifaccio, e per memorizzare l'elenco delle cartelle ho utilizzato un ArrayList, così come per le stringhe le espressioni regolari vanno sempre più che bene!

On Error Resume Next

Const HKEY_LOCAL_MACHINE = &H80000002
Const HKEY_USERS         = &H80000003

strComputer = "."

Set objRegistry = GetObject("winmgmts:\\" & strComputer & "\root\default:StdRegProv")
Set ws = CreateObject("Wscript.Shell")
Set temporaryFolders = CreateObject("System.Collections.ArrayList")
Set re = new RegExp

Function GetTemp(ProfilePath, K, Key)
On Error Resume Next
Temp = ws.RegRead(K & "\" & Key & "\Environment\TEMP")

If err.number = 0 Then
re.Pattern = "%USERPROFILE%"
Temp = re.Replace(Temp, ProfilePath)
Else
Temp = Null
End If

GetTemp = Temp
On Error Goto 0
End Function

strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
objRegistry.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubkeys

For Each objSubkey In arrSubkeys
rem *** legge il percorso del profilo ***
strValueName = "ProfileImagePath"
strSubPath = strKeyPath & "\" & objSubkey
objRegistry.GetExpandedStringValue HKEY_LOCAL_MACHINE, strSubPath, strValueName, strProfile

rem *** legge il percorso della cartella TEMP ***
FolderTmp = GetTemp(strProfile, "HKEY_USERS", objSubkey)

if isNull(FolderTmp) Then
rem *** non ha letto la chiave, quindi prova a caricare il registro da NTUSER.DAT ***
ws.Run """%WINDIR%\System32\REG.EXE"" LOAD HKLM\TempHive """ & strProfile & "\NTUSER.DAT""", 7, true
msgbox "Leggo " & strProfile
FolderTmp = GetTemp(strProfile, "HKEY_LOCAL_MACHINE", "TempHive")
msgbox "Letto " & FolderTmp
ws.Run """%WINDIR%\System32\REG.EXE"" UNLOAD HKLM\TempHive", 7, true
End If
temporaryFolders.Add FolderTmp
Next

For Each folder In temporaryFolders
Wscript.Echo "Percorso temporaneo da svuotare: " & folder
Next

Come al solito il codice è brutto sporco e cattivo, inoltre in caso di errore.... be meglio verificare per bene prima di eliminare, non si sa mai! Ovviamente posso estendere questo codice, ed aggiungere i percorsi temporanei di Internet Explorer, Mozilla Firefox, oppure qualsiasi altro, basta solo conoscere la posizione di registro che indica il percorso dove sono salvati, oppure basta solo conoscere l'eventuale percorso del file di configurazione presente nel profilo.

Ah forse lo stesso risultato lo si poteva ottenere solamente con un file batch, qualche FOR e molte chiamate all'utility REG.EXE: sarebbe stato più coerente, ma sicuramente molto più illeggibile!

Attenzione
Nel caso in cui vengano utilizzati dei profili roaming, è sempre un rischio andare a toccare i profili salvati in locale, poiché si rischia di far perdere al sistema le informazioni di quale sia l'ultimo profilo utilizzato.

Modifica ACL tramite script

Spesso mi capita di dover mantenere una struttura di cartelle di Windows che hanno delle ACL personalizzate con un livello di dettaglio molto elevato. Quando i permessi cominciano a diventare complessi lo strumento standard di Windows diventa scomodo ed è facile commettere degli errori.

L'utility Xcacls.exe, che si scarica dal sito della Microsoft, può essere di grande aiuto in tutti questi casi. Ho preparato uno script batch che richiama questa utility, leggendo da un file di testo con l'elenco delle cartelle e delle relative ACL da impostare ad ognuna di esse:

esempio di elencoacl.txt:

"c:\sede\ced";"DOMAIN\user1:C" "DOMAIN\user2:R"
"c:\sede\personale";"DOMAIN\user3:C" "DOMAIN\user4:R"
"c:\sede\personale\dirigenza";"DOMAIN\user3:C"
"c:\sede\personale\pub";"DOMAIN\Everyone:R"
"c:\sede\tecnico";"DOMAIN\user4:C" "DOMAIN\user5:C"
"c:\sede\tecnico\progetti";"DOMAIN\user4:R"
"c:\sede\tecnico\lavori";"DOMAIN\user5:R"

etc...
con il seguente impostaacl.cmd:

set LETTURA="DOMAIN\leggi:R"
set SCRITTURA="DOMAIN\fede:C"
set FULL="BUILTIN\Administrators:F" "NT AUTHORITY\SYSTEM:F"

for /F "tokens=1,2 delims=;" %%a in (elencoacl.txt) do (
  xcacls %%a /T /G %FULL% %LETTURA% %SCRITTURA% %%b /Y
  @if ERRORLEVEL 1 (
    echo Errore set ACL directory %%a
    pause
  )
)

Questo script batch legge in sequenza tutte le righe del file elencoacl.txt , ed imposta ricorsivamente ad ogni cartella i permessi associati. Fare attenzione alle cartelle innestate: questo script non è molto "furbo" e ripete le impostazioni più volte. Verificare che l'ordine delle cartelle sia corretto!