Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created March 20, 2019 01:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jborean93/05b7605a0269ac37c8d829ce65247875 to your computer and use it in GitHub Desktop.
Save jborean93/05b7605a0269ac37c8d829ce65247875 to your computer and use it in GitHub Desktop.
Enumerate Shadow Copies and mount them
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @'
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool CreateSymbolicLinkW(
string lpSymlinkFileName,
string lpTargetFileName,
UInt32 dwFlags);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool RemoveDirectoryW(
string lpPathName);
'@
Function Get-LastWin32ExceptionMessage {
param([int]$ErrorCode)
$Exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
$ExpMsg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $Exp.Message, $ErrorCode
return $ExpMsg
}
Function Get-ShadowCopy {
[OutputType('ShadowCopyInfo')]
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[System.String]
$Path
)
$CommonParameters = @{}
$PathUri = [System.Uri]$Path
if ($PathUri.IsUnc) {
$CommonParameters.ComputerName = $PathUri.DnsSafeHost
$ShareName = $Path.Split('\', 3, [System.StringSplitOptions]::RemoveEmptyEntries)[1]
$ShareParameters = @{
ClassName = "Win32_Share"
Filter = "Name='$($ShareName)'"
Property = @("Path")
}
$ShareParameters += $CommonParameters
$Share = Get-CimInstance @ShareParameters
if (-not $Share) {
Write-Error -Message "Failed to find share '$($ShareName)' for path '$Path'"
return
}
$PathRoot = [System.IO.Path]::GetPathRoot($Share.Path)
} else {
$PathRoot = [System.IO.Path]::GetPathRoot($Path)
}
$VolumeParameters = @{
ClassName = "Win32_Volume"
Filter = "DriveLetter='$($PathRoot.Substring(0, $PathRoot.Length - 1))'"
Property = @("DeviceID", "Name")
}
$Volume = Get-CimInstance @VolumeParameters
if (-not $Volume) {
Write-Error -Message "Failed to find volume with the drive '$PathRoot' for path '$Path'"
return
}
$ShadowParameters = @{
ClassName = "Win32_ShadowCopy"
Filter = "VolumeName='$($Volume.DeviceID.Replace('\', '\\'))'"
Property = @("DeviceObject", "InstallDate", "OriginatingMachine")
}
$ShadowParameters += $CommonParameters
$ShadowCopies = Get-CimInstance @ShadowParameters
if (-not $ShadowCopies) {
Write-Error -Message "Failed to find shadow copies for volume '$($Volume.DeviceID)' for path '$Path'"
return
}
foreach ($ShadowCopy in $ShadowCopies) {
[PSCustomObject]@{
PSTypeName = "ShadowCopyInfo"
Path = $Path
DeviceObject = $ShadowCopy.DeviceObject
DateModified = $ShadowCopy.InstallDate
VolumeName = $Volume.Name
ComputerName = $ShadowCopy.OriginatingMachine
}
}
}
Function Mount-ShadowCopy {
Param (
[Parameter(Mandatory = $true)]
[PSTypeName('ShadowCopyInfo')]
$ShadowCopy,
[System.String]
$MountPath,
[Switch]
$Force
)
if (-not $MountPath) {
$LeafName = "VSS Mount"
$MountPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $LeafName
}
$MountPath = "{0} ({1})" -f $MountPath, $ShadowCopy.DateModified.ToString("O").Replace(":", "_")
if (Test-Path -LiteralPath $MountPath) {
if ($Force) {
# Remove-Item fails if the symlink is broken, use PInvoke to bypass this
$Result = [Win32.NativeMethods]::RemoveDirectoryW($MountPath)
if (-not $Result) {
$Msg = Get-LastWin32ExceptionMessage -ErrorCode $LastError
Write-Error -Message "RemoveDirectoryW('$MountPath') failed - $Msg"
return
}
} else {
Write-Error -Message "Failed to mount shadow copy for $($ShadowCopy.Path) to $MountPath as the path already exists"
return
}
}
# The link target must end with "\" or else we cannot access it
$Result = [Win32.NativeMethods]::CreateSymbolicLinkW($MountPath, $ShadowCopy.DeviceObject + "\", 1)
if (-not $Result) {
$Msg = Get-LastWin32ExceptionMessage -ErrorCode $LastError
Write-Error -Message "CreateSymbolicLinkW('$MountPath', '$($ShadowCopy.DeviceObject)') failed - $Msg"
}
return $MountPath
}
Function Using-ShadowCopy {
<#
.SYNOPSIS
Automatically mounts and dismounts a shadow copy object for use in the PowerShell script.
.PARAMETER ShadowCopy
The ShadowCopyInfo object to mount, this can be retrieved with the Get-ShadowCopy cmdlet. This will only work for
shadow copies local to the system.
.PARAMETER Script
A script to execute in the mounted context. The path can be accessed with $MountedPath
.EXAMPLE
$ShadowCopies = Get-ShadowCopy -Path "C:\"
foreach ($ShadowCopy in $ShadowCopies) {
Using-ShadowCopy $ShadowCopy {
Get-ChildItem -LiteralPath $MountedPath
}
}
#>
Param (
[Parameter(Mandatory = $true, Position = 0)]
[PSTypeName('ShadowCopyInfo')]
$ShadowCopy,
[Parameter(Mandatory = $true, Position = 1)]
[ScriptBlock]
$Script
)
$MountPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
try {
$MountedPath = Mount-ShadowCopy -ShadowCopy $ShadowCopy -MountPath $MountPath
&$Script
} finally {
$Result = [Win32.NativeMethods]::RemoveDirectoryW($MountedPath)
if (-not $Result) {
$Msg = Get-LastWin32ExceptionMessage -ErrorCode $LastError
Write-Error -Message "RemoveDirectoryW('$MountedPath') failed - $Msg"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment