#Requires -Version 5.1 [CmdletBinding()] param( [Parameter(Mandatory)][ValidateScript({Test-Path -LiteralPath $_ -PathType Leaf})][string]$Path, [Parameter(Mandatory)][ValidatePattern('^\d+\.\d+\.\d+$')][string]$Version ) Set-StrictMode -Version Latest $ErrorActionPreference='Stop' $resolved=[IO.Path]::GetFullPath($Path) $bytes=[IO.File]::ReadAllBytes($resolved) if($bytes.Length-lt 1024){throw 'Executable is unexpectedly small.'} if($bytes[0]-ne0x4d-or$bytes[1]-ne0x5a){throw 'DOS header is missing.'} $pe=[BitConverter]::ToInt32($bytes,0x3c) if($pe-lt0-or($pe+96)-ge$bytes.Length-or[BitConverter]::ToUInt32($bytes,$pe)-ne0x00004550){throw 'PE header is invalid.'} if([BitConverter]::ToUInt16($bytes,$pe+4)-ne0x8664){throw 'Executable is not AMD64.'} $optional=$pe+24 if([BitConverter]::ToUInt16($bytes,$optional)-ne0x20b){throw 'Executable is not PE32+.'} if([BitConverter]::ToUInt16($bytes,$optional+68)-ne2){throw 'Executable is not a Windows GUI application.'} function Read-U16([byte[]]$Data,[int]$Offset){[BitConverter]::ToUInt16($Data,$Offset)} function Read-U32([byte[]]$Data,[int]$Offset){[BitConverter]::ToUInt32($Data,$Offset)} function Convert-Rva([byte[]]$Data,[int]$SectionTable,[int]$SectionCount,[uint32]$Rva){ for($index=0;$index-lt$SectionCount;$index++){ $section=$SectionTable+($index*40);$virtualSize=Read-U32 $Data ($section+8);$virtualAddress=Read-U32 $Data ($section+12);$rawSize=Read-U32 $Data ($section+16);$raw=Read-U32 $Data ($section+20) $span=[Math]::Max([long]$virtualSize,[long]$rawSize) if([long]$Rva-ge[long]$virtualAddress-and[long]$Rva-lt([long]$virtualAddress+$span)){return [int]([long]$raw+([long]$Rva-[long]$virtualAddress))} } throw 'Resource RVA is outside the PE sections.' } function Get-ResourceEntries([byte[]]$Data,[int]$Directory){ $count=(Read-U16 $Data ($Directory+12))+(Read-U16 $Data ($Directory+14));$rows=@() for($index=0;$index-lt$count;$index++){$offset=$Directory+16+($index*8);$rows+=,[pscustomobject]@{Name=Read-U32 $Data $offset;Target=Read-U32 $Data ($offset+4)}} $rows } function Get-IdEntry([byte[]]$Data,[int]$Directory,[uint32]$Id){ $match=@(Get-ResourceEntries $Data $Directory|Where-Object{([int64]$_.Name-band0x80000000L)-eq0-and($_.Name-band0xffff)-eq$Id}) if($match.Count-ne1){throw "PE resource ID $Id was not found exactly once."};$match[0] } $sectionCount=Read-U16 $bytes ($pe+6);$optionalSize=Read-U16 $bytes ($pe+20);$sectionTable=$optional+$optionalSize $resourceRva=Read-U32 $bytes ($optional+128) if($resourceRva-eq0){throw 'PE resource table is missing.'} $resourceBase=Convert-Rva $bytes $sectionTable $sectionCount $resourceRva $type=Get-IdEntry $bytes $resourceBase 24 if(([int64]$type.Target-band0x80000000L)-eq0){throw 'Manifest type does not reference a resource directory.'} $nameDirectory=$resourceBase+[int]([int64]$type.Target-band0x7fffffffL) $name=Get-IdEntry $bytes $nameDirectory 1 if(([int64]$name.Target-band0x80000000L)-eq0){throw 'Manifest name does not reference a language directory.'} $languageDirectory=$resourceBase+[int]([int64]$name.Target-band0x7fffffffL) $languages=@(Get-ResourceEntries $bytes $languageDirectory) if($languages.Count-lt1-or([int64]$languages[0].Target-band0x80000000L)-ne0){throw 'Manifest language entry is invalid.'} $dataEntry=$resourceBase+[int]([int64]$languages[0].Target-band0x7fffffffL) $manifestRva=Read-U32 $bytes $dataEntry;$manifestSize=Read-U32 $bytes ($dataEntry+4) $manifestOffset=Convert-Rva $bytes $sectionTable $sectionCount $manifestRva if($manifestSize-eq0-or([long]$manifestOffset+$manifestSize)-gt$bytes.Length){throw 'Embedded application manifest is invalid.'} $manifestBytes=[byte[]]::new($manifestSize);[Array]::Copy($bytes,$manifestOffset,$manifestBytes,0,$manifestSize) $manifest=[Text.Encoding]::UTF8.GetString($manifestBytes) if($manifest-notmatch'requestedExecutionLevel\s+level="requireAdministrator"'){throw 'Executable does not request administrator rights.'} $info=[Diagnostics.FileVersionInfo]::GetVersionInfo($resolved) if($info.FileVersion-notlike"$Version*"){throw "Unexpected file version: $($info.FileVersion)"} $checksumPath=$resolved+'.sha256' if(-not(Test-Path -LiteralPath $checksumPath -PathType Leaf)){throw 'Adjacent SHA-256 file is missing.'} $expected=(([IO.File]::ReadAllText($checksumPath)-split'\s+')[0]).ToLowerInvariant() $actual=(Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash.ToLowerInvariant() if($expected-ne$actual){throw 'Executable SHA-256 does not match the adjacent checksum.'} $signature=Get-AuthenticodeSignature -LiteralPath $resolved if($signature.Status-notin@('NotSigned','Valid')){throw "Unexpected Authenticode status: $($signature.Status)"} Write-Output "Executable inspection passed: AMD64 GUI, requireAdministrator, version $Version, SHA-256 $actual, signature $($signature.Status)."