PowerShell ile Zombi Dosyalarını Bulma


Bu konuya girmeden önce bir şeyin altını çizelim. Gönderilerimin çoğu doğası gereği eğitici olmayı amaçlamaktadır. Kavramları ve teknikleri öğretmek için genellikle 0 baytlık dosyaları bulmak gibi bir senaryoyu başlangıç noktası olarak alırım. Bir görevi başarmanın neredeyse her zaman birden fazla yolu vardır. Kodumun hiçbir zaman üretimde olduğu gibi çalıştırılması amaçlanmamıştır. Bunun yerine, bir şeyi nasıl yaptığımı ve neden yaptığımı ortadan kaldırmalısınız. Kavramlar ve teknikler görünürdeki hedefi çözmekten daha önemlidir.

CIM'i kullanma

Bu görev için Get-CimInstance'ı kullanmayı düşündüm. Farkında olmayabilirsiniz ama CIM_Veri Dosyası. Çoğu durumda ve dürüst olmak gerekirse durumun böyle olmayacağı bir durumdan emin değilim, tüm dosyalar CIM deposunda kayıtlıdır. İşte bu nesnelerden birinin neye benzediği:

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        : 

Diyelim ki C:\work\samples içindeki dosya boyutu 0 olan tüm dosyaları bulmak istiyorum. Şu şekilde bir WMI filtresi yazabilirim:

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

WMI/CIM sorguları oluştururken ters eğik çizgilerden kaçmanız gerekir, böylece \work\samples\ yol değeri \\work\\samples\\ olur. Ayrıca WMI/CIM sorgusundaki operatörlerin PowerShell değil eski operatörler olduğunu unutmayın.

PowerShell'de erken filtrelemenin önemini vurguluyoruz. Bir komut filtrelemenin bir yolunu sunuyorsa, onu kullanın. Erken filtreleme neredeyse her zaman her şeyi alıp ardından Nesne-Nereye iletmekten daha iyidir. Bu Where -Object kullanmanın kötü bir şey olduğu anlamına gelmez. Bazen başka seçeneğiniz olmaz veya kasıtlı olarak Where-Object'e yönlendirmeniz gerekir. Where-Object'i neden kullandığınızı bildiğinizden emin olun.

İşte benim basit testim.

Temel sorgumun işe yaradığını biliyorum ve beklediğim sonucu alıyorum. Şimdi bir adım geri atıp 0 uzunluktaki tüm dosyaları C:\Work dizininde dosyalamak istiyorum.

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

Hımmm. Bu sorgu, dosyayı C:\Work dizininin kökünde buldu ancak diğer dosyaları bulamadı. WMI sorgularınızı ne kadar spesifik hale getirirseniz genellikle o kadar hızlı çalışırlar. Ancak bu durumda joker karakter kullanarak sorguyu biraz genişletmem gerekiyor. Yolun \\work ile başladığı dosyaları bulmak istiyorum.

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

Bir WMI/Cim sorgusunda, joker karakter olarak geleneksel * yerine % karakterini kullanın. Ayrıca operatörü LIKE olarak değiştirdiğimi de unutmayın. Operatörler büyük/küçük harfe duyarlı değildir. Dikkat çekmesi için büyük harf kullandım. Bu yaklaşımın önemli bir dezavantajı "eşittir" karşılaştırmalarını kullanmaktan çok daha yavaş olmasıdır. Ama sonunda işe yaramalı.

Uyarılar ve İpuçları

WMI/CIM sorgularını kullanırken akılda tutulması gereken birkaç nokta vardır. İlk olarak, her bir nesnenin (bu vaka dosyasında) CIM deposuna kaydedildiğine dair bir garanti yoktur. Sorgunuzun dosyaları kaçırması teorik olarak mümkündür. Bu özel kullanım durumunda, klasörleri her zaman Get-ChildItem ile numaralandırabilirsiniz.

Bunun üzerinde çalışırken, eğer klasör gerçekten bir yeniden ayrıştırma noktası kullanıyorsa sorgunun çok daha uzun sürdüğünü ve hatta beklendiği gibi çalışmayabileceğini keşfettim. Örneğin, C:\Scripts klasörüm aslında OneDrive'a bir bağlantıdır.

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

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

Sorgumda C:\Scripts kullanırsam eksik sonuçlar aldığımı fark ettim. Ancak D:\OneDrive\Scripts kullanırsam beklediğim sonuçları çok daha hızlı alırdım.

Süreci hızlandırmanın bir diğer yolu da Get-CimInstance'ın alması gereken özellikleri sınırlamaktır. Bu başka bir filtreleme türüdür. 0 baytlık dosyalar için ilk kez C:\Work'ü aradığımda bu işlem 8 dakika sürdü. Ancak yalnızca birkaç özelliğe ihtiyacım olduğundan komutumu hassaslaştırabilirim:

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

Aynı sonuçları aldım ama bu sefer 39 saniyede. Büyük sorguların bazen uzun sürebileceğini, bazen de beklediğinizden daha hızlı çalışabileceğini kabul etmeliyim. Kural olarak LIKE kullanmak sorguyu her zaman yavaşlatır.

Fonksiyon Oluşturma

Tüm bunları aklımda tutarak, belirli bir yoldaki 0 baytlık dosyaları bulmaya yönelik bir işlev hazırladım.

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
}

Umarım yorumlar ne yaptığımı ve nedenini açıklar. Sezon için eğlenceli bir komut takma adı bile ekledim.

Fonksiyonda sonuçları PowerShell'de kullanabilmem için boru hattına özel bir nesne yazıyorum.

C:\ işlevini yinelemeli olarak çalıştırdım ve 1774 dosyayı bulmak yaklaşık 41 saniye sürdü. Bu bana oldukça verimli görünüyor!

Özet

Herhangi bir göreve yaklaşırken her zaman konsolda çalıştırabileceğiniz basit bir komutla başlayın. Bir komutu istemden başarıyla çalıştıramazsanız, onun etrafında bir işlev veya komut dosyası yazmakta zorlanacaksınız. Ben bile ilk önce fonksiyonu yazmadım. Bu, özellikle CIM cmdlet'lerini kullanırken ve zarif ve etkili bir sorgu bulmaya çalışırken geçerlidir.

PowerShell komut dosyası oluşturma hakkında daha fazla bilgi edinmek istiyorsanız PowerShell Komut Dosyası Oluşturma ve Araç Yapımı'nın bir kopyasını almayı düşünün. Ne yaptığım veya neden yaptığım hakkında sorularınız varsa, lütfen yorum bırakmaktan çekinmeyin.

İlgili makaleler