Búsqueda de sitios web

Encontrar archivos Zombie con PowerShell


Antes de profundizar en esto, permítanme enfatizar algo. Muchas de mis publicaciones tienen como objetivo ser de naturaleza educativa. A menudo tomo un escenario, como encontrar archivos de 0 bytes, como punto de partida para enseñar conceptos y técnicas. Casi siempre hay varias formas de realizar una tarea. Mi código nunca está destinado a ejecutarse en producción tal como está. Más bien, deberías quitar cómoestoy haciendo algo y por qué. Los conceptos y técnicas son más importantes que resolver el objetivo aparente.

Usando CIM

Para esta tarea, pensé en utilizar Get-CimInstance. Puede que no lo sepas, pero hay una clase WMI que puedes consultar para buscar archivos en una computadora llamada CIM_Datafile. En la mayoría de las situaciones, y para ser honesto, no estoy seguro de cuál no sería el caso, todos los archivos se registran en el repositorio CIM. Así es como se ve uno de estos objetos:

Status                : OK
Name                  : c:\work\samples\w.txt
Caption               : c:\work\samples\w.txt
Description           : c:\work\samples\w.txt
InstallDate           : 10/9/2020 3:35:49 PM
AccessMask            : 18809343
Archive               : True
Compressed            : False
CompressionMethod     : 
CreationClassName     : CIM_LogicalFile
CreationDate          : 10/9/2020 3:35:49 PM
CSCreationClassName   : Win32_ComputerSystem
CSName                : PROSPERO
Drive                 : c:
EightDotThreeFileName : c:\work\samples\w.txt
Encrypted             : False
EncryptionMethod      : 
Extension             : txt
FileName              : w
FileSize              : 0
FileType              : Text Document
FSCreationClassName   : Win32_FileSystem
FSName                : NTFS
Hidden                : False
InUseCount            : 
LastAccessed          : 10/30/2020 11:49:34 AM
LastModified          : 10/9/2020 6:56:31 PM
Path                  : \work\samples\
Readable              : True
System                : False
Writeable             : True
Manufacturer          : 
Version               : 
PSComputerName        : 

Digamos que quiero encontrar todos los archivos en C:\work\samples que tengan un tamaño de archivo de 0. Puedo escribir un filtro WMI como este:

"Drive='c:' AND path = '\\work\\samples\\' AND filesize=0"

Al crear consultas WMI/CIM, debe evitar las barras invertidas, por lo que el valor de la ruta \work\samples\ se convierte en \\work\\samples\\. Recuerde también que los operadores en una consulta WMI/CIM son operadores heredados, no de PowerShell.

En PowerShell, destacamos la importancia de filtrar temprano. Si un comando ofrece una forma de filtrar, úselo. El filtrado temprano casi siempre es mejor que obtener todo y luego canalizarlo a Dónde-Objeto. Esto no quiere decir que usar Where -Object sea algo malo. A veces no tienes otra opción, o necesitas intencionalmente conectarte a Where-Object. Sólo asegúrese de saber por qué está utilizando Where-Object.

Aquí está mi prueba sencilla.

Sé que mi consulta básica funciona y obtengo el resultado que espero. Ahora, quiero dar un paso atrás y archivar todos los archivos de longitud 0 en C:\Work.

Get-CimInstance -ClassName CIM_Datafile -Filter "Drive='c:' AND path = '\\work\\' AND filesize=0" | Select-Object Name,FileSize

Mmm. Esta consulta encontró el archivo en la raíz de C:\Work, pero no encontró los otros archivos. Cuanto más específicas pueda realizar sus consultas WMI, normalmente más rápido se ejecutarán. Sin embargo, en este caso, necesito ampliar un poco la consulta utilizando un comodín. Quiero encontrar archivos donde la ruta comience con \\work.

Get-CimInstance -ClassName CIM_Datafile -Filter "Drive='c:' AND path LIKE '\\work\\%' AND filesize=0" | Select-Object Name,FileSize

En una consulta WMI/Cim, utilice % como carácter comodín en lugar del tradicional *. También tenga en cuenta que cambié el operador a ME GUSTA. Los operadores no distinguen entre mayúsculas y minúsculas. Usé mayúsculas para que se resaltaran. Un inconveniente importante de este enfoque es que es mucho más lento que utilizar comparaciones "iguales a". Pero eventualmente debería funcionar.

Advertencias y consejos

Al utilizar consultas WMI/CIM, hay algunas cosas que se deben tener en cuenta. Primero, no hay garantía de que cada objeto (en este caso, archivo) haya sido registrado en el repositorio CIM. En teoría, es posible que en su consulta se omitan archivos. En este caso de uso específico, siempre puede enumerar carpetas con Get-ChildItem.

Mientras trabajaba en esto, descubrí que si la carpeta realmente usa un punto de análisis, la consulta demora considerablemente más y es posible que ni siquiera funcione como se esperaba. Por ejemplo, mi carpeta C:\Scripts es en realidad un enlace a OneDrive.

PS C:\> get-item c:\scripts | Select Target

Target
------
{D:\OneDrive\Scripts}

Descubrí que si usaba C:\Scripts en mi consulta, obtenía resultados incompletos. Pero si usé D:\OneDrive\Scripts obtuve los resultados que esperaba mucho más rápido.

Otra forma de acelerar el proceso es limitar las propiedades que Get-CimInstance necesita obtener. Este es otro tipo de filtrado. La primera vez que busqué archivos de 0 bytes en C:\Work, me llevó 8 minutos. Pero como sólo necesito unas pocas propiedades, puedo refinar mi comando:

Get-CimInstance -ClassName CIM_Datafile -Filter "Drive='c:' AND path LIKE '\\work\\%' AND filesize=0" -property Name,FileSize | Select-Object Name,FileSize

Obtuve los mismos resultados pero esta vez en 39 segundos. Admito que las consultas grandes a veces pueden llevar mucho tiempo y, a veces, ejecutarse más rápido de lo esperado. Aunque por regla general, usar LIKE siempre ralentiza la consulta.

Creando una función

Con todo esto en mente, preparé una función para buscar archivos de 0 bytes en una ruta determinada.

Function Get-ZeroLengthFiles {
    [CmdletBinding()]
    [alias("gzf", "zombie")]
    Param(
        [Parameter(Position = 0)]
        [ValidateScript( { Test-Path -Path $_ })]
        [string]$Path = ".",
        [switch]$Recurse
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
        #select a subset of properties which speeds things up
        $get = "Name", "CreationDate", "LastModified", "FileSize"

        $cimParams = @{
            Classname   = "CIM_DATAFILE"
            Property    = $get
            ErrorAction = "Stop"
            Filter      = ""
        }
    } #begin
    Process {
         Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using specified path $Path"
         #test if folder is using a link or reparse point
         if ( (Get-Item -path $Path).Target) {
             $target =  (Get-Item -path $Path).Target
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] A reparse point was detected pointing towards $target"
            #re-define $path to use the target
            $Path = $Target
         }
        #convert the path to a file system path
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Converting $Path"
        $cPath = Convert-Path $Path
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Converted to $cPath"

        #trim off any trailing \ if cPath is other than a drive root like C:\
        if ($cpath.Length -gt 3 -AND $cpath -match "\\$") {
            $cpath = $cpath -replace "\\$", ""
        }

        #parse out the drive
        $drive = $cpath.Substring(0, 2)
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using Drive $drive"

        #get the folder path from the first \
        $folder = $cpath.Substring($cpath.IndexOf("\")).replace("\", "\\")
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using folder $folder (escaped)"

        if ($folder -match "\w+" -AND $PSBoundParameters.ContainsKey("Recurse")) {
            #create the filter to use the wildcard for recursing
            $filter = "Drive='$drive' AND Path LIKE '$folder\\%' AND FileSize=0"
        }
        elseif ($folder -match "\w+") {
            #create an exact path pattern
            $filter = "Drive='$drive' AND Path='$folder\\' AND FileSize=0"
        }
        else {
            #create a root drive filter for a path like C:\
            $filter = "Drive='$drive' AND Path LIKE '\\%' AND FileSize=0"
        }

        #add the filter to the parameter hashtable
        $cimParams.filter = $filter
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Looking for zero length files with filter $filter"

        #initialize a counter to keep track of the number of files found
        $i=0
        Try {
            Write-Host "Searching for zero length files in $cpath. This might take a few minutes..." -ForegroundColor magenta
            #find files matching the query and create a custom object for each
            Get-CimInstance @cimParams | ForEach-Object {
                #increment the counter
                $i++

                #create a custom object
                [PSCustomObject]@{
                    PSTypeName   = "cimZeroLengthFile"
                    Path         = $_.Name
                    Size         = $_.FileSize
                    Created      = $_.CreationDate
                    LastModified = $_.LastModified
                }
            }
        }
        Catch {
            Write-Warning "Failed to run query. $($_.exception.message)"
        }
        if ($i -eq 0) {
            #display a message if no files were found
            Write-Host "No zero length files were found in $cpath." -ForegroundColor yellow
        }
        else {
             Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Found $i matching files"
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

Espero que los comentarios expliquen qué estoy haciendo y por qué. Incluso agregué un alias de comando divertido para la temporada.

En la función, estoy escribiendo un objeto personalizado en la canalización para poder usar los resultados en PowerShell.

Ejecuté la función para C:\ de forma recursiva y me llevó unos 41 segundos encontrar 1774 archivos. ¡Eso me parece bastante eficiente!

Resumen

Al abordar cualquier tarea, comience siempre con un comando simple que pueda ejecutar en la consola. Si no puede ejecutar un comando correctamente desde un mensaje, tendrá dificultades para escribir una función o un script a su alrededor. Ni siquiera yo escribí la función primero. Esto es especialmente cierto cuando se utilizan cmdlets CIM y se intenta generar una consulta elegante y eficiente.

Si desea obtener más información sobre las secuencias de comandos de PowerShell, considere obtener una copia de PowerShell Scripting and Toolmaking. Si tiene alguna pregunta sobre lo que hice o por qué, no dude en dejar un comentario.

Artículos relacionados