ArtifactRetrieval/AppCompatCache.ps1
|
function Get-CSAppCompatCache { <# .SYNOPSIS Retrieves and parses entries from the AppCompatCache based on OS version. Author: Jesse Davis (@secabstraction) License: BSD 3-Clause .DESCRIPTION Get-CSAppCompatCache parses entries from the Application Compatibility Cache stored in the registry. .PARAMETER CimSession Specifies the CIM session to use for this cmdlet. Enter a variable that contains the CIM session or a command that creates or gets the CIM session, such as the New-CimSession or Get-CimSession cmdlets. For more information, see about_CimSessions. .EXAMPLE Get-CSAppCompatCache .EXAMPLE Get-CSAppCompatCache -CimSession $CimSession .OUTPUTS CimSweep.AppCompatCacheEntry Outputs objects consisting of the application's file path and that file's last modified time. Note: the LastModified property is a UTC datetime string in Round-trip format. #> [CmdletBinding()] [OutputType('CimSweep.AppCompatCacheEntry')] param ( [Alias('Session')] [ValidateNotNullOrEmpty()] [Microsoft.Management.Infrastructure.CimSession[]] $CimSession ) begin { # If a CIM session is not provided, trick the function into thinking there is one. if (-not $PSBoundParameters['CimSession']) { $CimSession = '' $CIMSessionCount = 1 } else { $CIMSessionCount = $CimSession.Count } $CurrentCIMSession = 0 } process { foreach ($Session in $CimSession) { $ComputerName = $Session.ComputerName if (-not $Session.ComputerName) { $ComputerName = 'localhost' } # Display a progress activity for each CIM session Write-Progress -Id 1 -Activity 'CimSweep - AppCompatCache sweep' -Status "($($CurrentCIMSession+1)/$($CIMSessionCount)) Current computer: $ComputerName" -PercentComplete (($CurrentCIMSession / $CIMSessionCount) * 100) $CurrentCIMSession++ $CommonArgs = @{} if ($Session.Id) { $CommonArgs['CimSession'] = $Session } $OS = Get-CimInstance -ClassName Win32_OperatingSystem @CommonArgs if ($OS.Version -like "5.1*") { $Parameters = @{ Hive = 'HKLM' SubKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatibility' } } else { $Parameters = @{ Hive = 'HKLM' SubKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache' ValueName = 'AppCompatCache' } } $AppCompatCacheValue = Get-CSRegistryValue @Parameters @CommonArgs ConvertFrom-ByteArray -CacheValue $AppCompatCacheValue -OSVersion $OS.Version -OSArchitecture $OS.OSArchitecture } } end {} } function ConvertFrom-ByteArray { <# .SYNOPSIS Converts bytes from the AppCompatCache registry key into objects. Author: Jesse Davis (@secabstraction) License: BSD 3-Clause Thanks to @ericrzimmerman for these test files https://github.com/EricZimmerman/AppCompatCacheParser/tree/master/AppCompatCacheParserTest/TestFiles .DESCRIPTION ConvertFrom-ByteArray converts bytes from the AppCompatCache registry key into objects. .PARAMETER CacheValue Byte array from the AppCompatCache registry key. .PARAMETER OSVersion Specifies the operating system version from which the AppCompatCache bytes were retrieved. .PARAMETER OSArchitecture Specifies the bitness of the operating system from which the AppCompatCache bytes were retrieved. .EXAMPLE ConvertFrom-ByteArray -CacheBytes $AppCompatCacheKeyBytes -OSVersion 6.1 -OSArchitecture 32-bit #> param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Object] $CacheValue, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $OSVersion, [Parameter()] [string] $OSArchitecture ) $BinaryReader = New-Object IO.BinaryReader (New-Object IO.MemoryStream (,$CacheValue.ValueContent)) $ASCIIEncoding = [Text.Encoding]::ASCII $UnicodeEncoding = [Text.Encoding]::Unicode switch ($OSVersion) { { $_ -like '10.*' } { # Windows 10 $null = $BinaryReader.BaseStream.Seek(48, [IO.SeekOrigin]::Begin) # check for magic if ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '10ts') { $null = $BinaryReader.BaseStream.Seek(52, [IO.SeekOrigin]::Begin) # offset shifted in creators update if ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '10ts') { throw 'Not Windows 10' } } do { # parse entries $null = $BinaryReader.BaseStream.Seek(8, [IO.SeekOrigin]::Current) # padding between entries $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes($BinaryReader.ReadUInt16())) $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime $null = $BinaryReader.ReadBytes($BinaryReader.ReadInt32()) # skip some bytes $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '10ts') } { $_ -like '6.3*' } { # Windows 8.1 / Server 2012 R2 $null = $BinaryReader.BaseStream.Seek(128, [IO.SeekOrigin]::Begin) # check for magic if ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '10ts') { throw 'Not windows 8.1/2012r2' } do { # parse entries $null = $BinaryReader.BaseStream.Seek(8, [IO.SeekOrigin]::Current) # padding & datasize $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes($BinaryReader.ReadUInt16())) $null = $BinaryReader.ReadBytes(10) # skip insertion/shim flags & padding $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime $null = $BinaryReader.ReadBytes($BinaryReader.ReadInt32()) # skip some bytes $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '10ts') } { $_ -like '6.2*' } { # Windows 8.0 / Server 2012 # check for magic $null = $BinaryReader.BaseStream.Seek(128, [IO.SeekOrigin]::Begin) if ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '00ts') { throw 'Not Windows 8/2012' } do { # parse entries $null = $BinaryReader.BaseStream.Seek(8, [IO.SeekOrigin]::Current) # padding & datasize $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes($BinaryReader.ReadUInt16())) $null = $BinaryReader.BaseStream.Seek(10, [IO.SeekOrigin]::Current) # skip insertion/shim flags & padding $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime $null = $BinaryReader.ReadBytes($BinaryReader.ReadInt32()) # skip some bytes $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($ASCIIEncoding.GetString($BinaryReader.ReadBytes(4)) -ne '00ts') } { $_ -like '6.1*' } { # Windows 7 / Server 2008 R2 # check for magic if ([BitConverter]::ToString($BinaryReader.ReadBytes(4)[3..0]) -ne 'BA-DC-0F-EE') { throw 'Not Windows 7/2008R2'} $NumberOfEntries = $BinaryReader.ReadInt32() $null = $BinaryReader.BaseStream.Seek(128, [IO.SeekOrigin]::Begin) # skip padding if ($OSArchitecture -eq '32-bit') { do { $EntryPosition++ $PathSize = $BinaryReader.ReadUInt16() $null = $BinaryReader.ReadUInt16() # MaxPathSize $PathOffset = $BinaryReader.ReadInt32() $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime $null = $BinaryReader.BaseStream.Seek(16, [IO.SeekOrigin]::Current) $Position = $BinaryReader.BaseStream.Position $null = $BinaryReader.BaseStream.Seek($PathOffset, [IO.SeekOrigin]::Begin) $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes($PathSize)) $null = $BinaryReader.BaseStream.Seek($Position, [IO.SeekOrigin]::Begin) $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($EntryPosition -eq $NumberOfEntries) } else { # 64-bit do { $EntryPosition++ $PathSize = $BinaryReader.ReadUInt16() # Padding $null = $BinaryReader.BaseStream.Seek(6, [IO.SeekOrigin]::Current) $PathOffset = $BinaryReader.ReadInt64() $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime $null = $BinaryReader.BaseStream.Seek(24, [IO.SeekOrigin]::Current) $Position = $BinaryReader.BaseStream.Position $null = $BinaryReader.BaseStream.Seek($PathOffset, [IO.SeekOrigin]::Begin) $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes($PathSize)) $null = $BinaryReader.BaseStream.Seek($Position, [IO.SeekOrigin]::Begin) $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($EntryPosition -eq $NumberOfEntries) } } { $_ -like '6.0*' } { <# Windows Vista / Server 2008 #> } { $_ -like '5.2*' } { <# Windows XP Pro 64-bit / Server 2003 (R2) #> } { $_ -like '5.1*' } { # Windows XP 32-bit # check for magic if ([BitConverter]::ToString($BinaryReader.ReadBytes(4)[3..0]) -ne 'DE-AD-BE-EF') { throw 'Not Windows XP 32-bit'} $NumberOfEntries = $BinaryReader.ReadInt32() # this is always 96, even if there aren't 96 entries $null = $BinaryReader.BaseStream.Seek(400, [IO.SeekOrigin]::Begin) # skip padding do { # parse entries $EntryPosition++ $Path = $UnicodeEncoding.GetString($BinaryReader.ReadBytes(528)).TrimEnd("`0") # 528 == MAX_PATH + 4 unicode chars $LastModifiedTime = [DateTimeOffset]::FromFileTime($BinaryReader.ReadInt64()).DateTime if (($LastModifiedTime.Year -eq 1600) -and !$Path) { break } # empty entries == end $null = $BinaryReader.BaseStream.Seek(16, [IO.SeekOrigin]::Current) # skip some bytes $ObjectProperties = [ordered] @{ PSTypeName = 'CimSweep.AppCompatCacheEntry' Path = $Path LastModifiedTime = $LastModifiedTime.ToUniversalTime().ToString('o') } if ($CacheValue.PSComputerName) { $ObjectProperties['PSComputerName'] = $CacheValue.PSComputerName } [PSCustomObject]$ObjectProperties } until ($EntryPosition -eq $NumberOfEntries) } } $BinaryReader.BaseStream.Dispose() $BinaryReader.Dispose() } Export-ModuleMember -Function Get-CSAppCompatCache |