PowerShell: Taking a Snapshot of a Windows Volume

I have this snippet embedded in various programs. Perhaps, it’s useful to be posted as an independent script to be refactored into other codes.

function createVssSnapshot{
    [cmdletbinding()]
    param(
        [string]$targetVolume="C:\",
        [string]$accessPath='C:\vssSnapshot',
        [bool]$openSnapshotAfter=$true
    )
    # Fix targetVolume input if it's missing the suffix
    if (!($targetVolume -like "*\")){$targetVolume+="\"}    
    $volumeLetter=$targetVolume[0]
    $driveLabel=(get-volume -DriveLetter $volumeLetter).FileSystemLabel
    $volumelabel="Volume_$volumeLetter$(if($driveLabel){'_'+$driveLabel})"
    $localSnapshotPath="$accessPath\$env:computername\$volumeLabel\$(Get-Date -Format 'yyyy-MM-dd_hh.mm.ss')"  
     
    # Create the VSS snapshot
    $shadowCopyClass=[WMICLASS]"root\cimv2:win32_shadowcopy";
    $thisSnapshot = $shadowCopyClass.Create($targetVolume, "ClientAccessible");
    $thisShadow = Get-WmiObject Win32_ShadowCopy | Where-Object { $_.ID -eq $thisSnapshot.ShadowID };
    $thisShadowPath  = $thisShadow.DeviceObject + "\";        

    # Create snapshot parent directory
    $null=New-Item -ItemType Directory -Force -Path $(Split-Path $localSnapshotPath -Parent);

    # Make links to this snapshot
    $null=cmd /c mklink /J $localSnapshotPath $thisShadowPath;    
     
    if(test-path $localSnapshotPath){
        write-host "Snapshot of $targetVolume has been made and it's accessible at this path: $localSnapshotPath"
        if($openSnapshotAfter){invoke-item $localSnapshotPath}
        return $true
    }else{
        write-warning "Something went wrong. Snapshot was not taken."
        return $false
        }
}
 
createVssSnapshot

Removing VSS Snapshots via PowerShell is classified as high-risk by most antivirus software – read my Active Directory PEN testing articles for more background on such logic. This is an example of an interception by “rule heuristic.b.14” by this flavor of anti-malware. Hence, the below script will not run on such hosts.

function deleteVssSnapshot{
    [cmdletbinding()]
    param(
        [string]$targetVolume="C:\",
        [string]$snapShotId,
        [bool]$removeAllSnapshots=$false
    )
    $ErrorActionPreference='stop'
    if(!(Test-WSMan)){Enable-PSRemoting –force}
 
    function removeOneSnapshot($snapShotId){
        # Remove a single snapshot is a function by itself because Antivirus will terminate any session that tries to remove a VSS snapshot
        write-host "Removing snapshot Id $snapShotId... Ctrl+C to Cancel"
        pause

        # This is the workaround to the annoyance of antivirus software terminating sessions upon invoking the snapshot removal procedure
        $session=New-PSSession
        Invoke-Command -Session $session -AsJob -ScriptBlock{param($snapShotId);$null=vssadmin delete shadows /Shadow=$snapShotId /quiet} -args $snapShotId|Receive-Job -Wait
        if($session.state -ne 'Opened'){                    
            Write-Warning "An error (probably antivirus heuristics detection) has prevented removal of snapshot ID $snapShotId"
            Remove-PSSession $session
            return $false
        }else{
            Remove-PSSession $session
            return $true
            }
        }

    if($removeAllSnapshots){
        try{
            #vssadmin delete shadows /For=$targetVolume /Quiet
            $volumeId=(get-volume|?{$_.DriveLetter -eq $targetVolume[0]}).UniqueId
            $targetSnapshotIds=Get-WmiObject Win32_ShadowCopy|?{$_.VolumeName -eq $volumeId}|%{[void]($_.__PATH -match '{(.*)}');($matches[1]).tolower()}
            Write-host "These snapshots IDs are detected on $targetVolume`r`n$(($targetSnapshotIds|out-string).trim())`r`n------------------------------`r`n"
            foreach ($targetSnapshotId in $targetSnapshotIds){
                $snapShotId="{$targetSnapshotId`}"
                $success=removeOneSnapshot $snapShotId
                if(!$success){
                    $remainingSnapshotIds=Get-WmiObject Win32_ShadowCopy|?{$_.VolumeName -eq $volumeId}|%{[void]($_.__PATH -match '{(.*)}');($matches[1]).tolower()}
                    Write-Warning "Program cannot proceed to remove these snapshots:`r`n$(($remainingSnapshotIds|out-string).trim())"
                    return $false
                    }
            }            
            $checkSnapshotIds=Get-WmiObject Win32_ShadowCopy|?{$_.VolumeName -eq $volumeId}|%{[void]($_.__PATH -match '{(.*)}');($matches[1]).tolower()}
            if($checkSnapshotIds){
                write-warning "These snapshot IDs still persists: $checkSnapshotIds"
                return $false
            }else{
                write-host "All snapshots of volume $targetVolume have been purged."
                return $true
                }
        }catch{
            return $false
            }
    }else{   
        # LIFO: select the last snapshot ID if it is not specified
        if(!($snapShotId)){
            $lastSnapshotIdString=vssadmin list shadows /for=$targetVolume|`
                            %{if($_ -like "*Shadow Copy ID:*"){$_}}|`
                            select-object -last 1|`
                            %{[void]($_ -match "{(.*)}$"); $matches[1]}
            $snapShotId="{$lastSnapshotIdString`}"
            }
        removeOneSnapshot $snapShotId

        # Validation
        #vssadmin list shadows /for=$targetVolume
        $validateLastSnapshot=vssadmin list shadows /for=$targetVolume|`
                        %{if($_ -like "*Shadow Copy ID:*"){$_}}|`
                        select-object -last 1|`
                        %{[void]($_ -match "{(.*)}$"); $matches[1]}
        $validateLastSnapshotId="{$validateLastSnapshot}"
        if(!($validateLastSnapshotId -eq $snapShotId)){
            write-host "Last snapshot Id is now $validateLastSnapshotId"
            return $true
            }else{
                write-host "$snapShotId still exists. There was an error in its removal";
                return $false
                }
    }
}
   
deleteVssSnapshot

Leave a Reply

Your email address will not be published. Required fields are marked *