Last active
August 19, 2020 10:08
-
-
Save rileyz/35d6390a7f23b514f0c16e829ed07db5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Script to assist with Cyber Essentials Plus compliance in relation to vulnerable CVE | |
executables. | |
.DESCRIPTION | |
Intended Use | |
This script was produced to automate replacing CVE executables via a MECM Configuration Item, | |
ensuring remediation without human interaction. | |
About | |
Written to save time and ensure compliance, as it is not humanly possible for a person to check | |
all files without error. | |
Known Defects/Bugs | |
* None known. | |
Code Snippet Credits | |
* https://etechgoodness.wordpress.com/2014/12/11/powershell-determine-if-an-exe-is-32-or-64-bit-and-other-tricks/ | |
* https://lazywinadmin.com/2014/09/powershell-tip-escape-regex.html | |
Version History | |
1.00 17/08/2020 | |
Initial release. | |
Copyright & Intellectual Property | |
Feel to copy, modify and redistribute, but please pay credit where it is due. | |
Feed back is welcome, please contact me on LinkedIn. | |
.LINK | |
Author:.......https://www.linkedin.com/in/rileylim | |
Source Code:..https://gist.github.com/rileyz/35d6390a7f23b514f0c16e829ed07db5 | |
Article:......https://www | |
#> | |
# Function List ################################################################################### | |
function Get-ExeTargetMachine | |
{ | |
<# | |
.SYNOPSIS Displays the machine type of any Windows executable. | |
.DESCRIPTION | |
Displays the target machine type of any Windows executable file (.exe or .dll). | |
The expected usage is to determine if an executable is 32- or 64-bit, in which case it will return "x86" or "x64", respectively. | |
However, all machine types that were known as of the date of this script's authoring are detected. | |
.PARAMETER Path | |
A string that contains the path to the file to be checked. Can be relative or absolute. | |
.PARAMETER IncludeFileName | |
If set, includes the file name in the displayed output. | |
.PARAMETER IgnoreInvalidFiles | |
Silently skips 16-bit or non-executable files. | |
.PARAMETER SuppressErrors | |
Errors (except invalid path) are not reported. | |
Warnings about 16-bit and non-PE files are still reported; use IgnoreInvalidFiles to suppress. | |
.LINK | |
http://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx | |
https://etechgoodness.wordpress.com/2014/12/11/powershell-determine-if-an-exe-is-32-or-64-bit-and-other-tricks/ | |
.OUTPUTS | |
If IncludeFileName is not specified, outputs a custom object with a TargetMachine property that contains a string with the executable's target machine type. | |
If IncludeFileName is specified, outputs a custom object with a Path property that contains the full path of the executable's name and a TargetMachine property that contains a string with the executable's target machine type. | |
.NOTES | |
Author: Eric Siron | |
Copyright: (C) 2014 Eric Siron | |
Version 1.0.1 November 3, 2015 Modified non-EXE handling to return as soon as further processing is unnecessary | |
Version 1.0 Authored Date: December 10, 2014 | |
.EXAMPLE PS C:\> Get-ExeTargetMachine C:\Windows\bfsvc.exe | |
Description | |
----------- | |
Returns a TargetMachine of x64 | |
.EXAMPLE | |
PS C:\> Get-ExeTargetMachine C:\Windows\winhlp32.exe | |
Description | |
----------- | |
Returns a TargetMachine of x86 | |
.EXAMPLE | |
PS C:\> Get-ChildItem 'C:\Program Files (x86)\*.exe' -Recurse | Get-ExeTargetMachine -IncludeFileName | |
Description | |
----------- | |
Returns the TargetMachine of all EXE files under C:\Program Files (x86) and all subfolders, displaying their complete path names along with their machine type. | |
.EXAMPLE | |
PS C:\> Get-ChildItem 'C:\Program Files\*.exe' -Recurse | Get-ExeTargetMachine -IncludeFileName | where { $_.TargetMachine -ne "x64" } | |
Description | |
----------- | |
Returns the Path and TargetMachine of all EXE files under C:\Program Files and all subfolders that are not 64-bit (x64). | |
.EXAMPLE | |
PS C:\> Get-ChildItem 'C:\windows\*.exe' -Recurse | Get-ExeTargetMachine | where { $_.TargetMachine -eq "" } | |
Description | |
----------- | |
Shows only errors and warnings for the EXE files under C:\Windows and subfolders. This can be used to find 16-bit and other EXEs that don't conform to the portable executable standard. | |
.EXAMPLE | |
PS C:\> Get-ChildItem 'C:\Program Files\' -Recurse | Get-ExeTargetMachine -IncludeFileName -IgnoreInvalidFiles -SuppressErrors | Out-GridView | |
Description | |
----------- | |
Finds every file in C:\Program Files and subfolders with a portable executable header, regardless of extension, and displays their names and Target Machine in a grid view. | |
#> | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] | |
[Alias("FullName")][String]$Path, | |
[Parameter()][Switch]$IncludeFileName = $false, | |
[Parameter()][Switch]$IgnoreInvalidFiles = $false, | |
[Parameter()][Switch]$SuppressErrors = $false | |
) | |
BEGIN { | |
## Constants ## | |
New-Variable -Name PEHeaderOffsetLocation -Option Constant -Value 0x3c | |
New-Variable -Name PEHeaderOffsetLocationNumBytes -Option Constant -Value 2 | |
New-Variable -Name PESignatureNumBytes -Option Constant -Value 4 | |
New-Variable -Name MachineTypeNumBytes -Option Constant -Value 2 | |
## Globals ## | |
$NonStandardExeFound = $false | |
} | |
PROCESS { | |
$Path = (Get-Item -Path $Path -ErrorAction Stop).FullName | |
try | |
{ | |
$PEHeaderOffset = New-Object Byte[] $PEHeaderOffsetLocationNumBytes | |
$PESignature = New-Object Byte[] $PESignatureNumBytes | |
$MachineType = New-Object Byte[] $MachineTypeNumBytes | |
Write-Debug "Opening $Path for reading." | |
try | |
{ | |
$FileStream = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) | |
} | |
catch | |
{ | |
if($SuppressErrors) | |
{ | |
return | |
} | |
throw $_ #implicit 'else' | |
} | |
Write-Debug "Moving to the header location expected to contain the location of the PE (portable executable) header." | |
$FileStream.Position = $PEHeaderOffsetLocation | |
$BytesRead = $FileStream.Read($PEHeaderOffset, 0, $PEHeaderOffsetLocationNumBytes) | |
if($BytesRead -eq 0) | |
{ | |
if($SuppressErrors) | |
{ | |
return | |
} | |
throw "$Path is not the correct format (PE header location not found)." #implicit 'else' | |
} | |
Write-Debug "Moving to the indicated position of the PE header." | |
$FileStream.Position = [System.BitConverter]::ToUInt16($PEHeaderOffset, 0) | |
Write-Debug "Reading the PE signature." | |
$BytesRead = $FileStream.Read($PESignature, 0, $PESignatureNumBytes) | |
if($BytesRead -ne $PESignatureNumBytes) | |
{ | |
if($IgnoreInvalidFiles) | |
{ | |
return | |
} | |
throw("$Path is not the correct format (PE Signature is an incorrect size).") # implicit 'else' | |
} | |
Write-Debug "Verifying the contents of the PE signature (must be characters `"P`" and `"E`" followed by two null characters)." | |
if(-not($PESignature[0] -eq [Char]'P' -and $PESignature[1] -eq [Char]'E' -and $PESignature[2] -eq 0 -and $PESignature[3] -eq 0)) | |
{ | |
if(-not($IgnoreInvalidFiles)) | |
{ | |
Write-Warning "$Path is 16-bit or is not a Windows executable." | |
} | |
return | |
} | |
Write-Debug "Retrieving machine type." | |
$BytesRead = $FileStream.Read($MachineType, 0, $MachineTypeNumBytes) | |
if($BytesRead -ne $MachineTypeNumBytes) | |
{ | |
if($SuppressErrors) | |
{ | |
return | |
} | |
throw "$Path appears damaged (Machine Type not correct size)." # implicit 'else' | |
} | |
$RawMachineType = [System.BitConverter]::ToUInt16($MachineType, 0) | |
$TargetMachine = switch ($RawMachineType) | |
{ | |
0x0 { 'Unknown' } | |
0x1d3 { 'Matsushita AM33' } | |
0x8664 { 'x64' } | |
0x1c0 { 'ARM little endian' } | |
0x1c4 { 'ARMv7 (or higher) thumb mode only' } | |
0xaa64 { 'ARMv8 in 64-bit mode' } | |
0xebc { 'EFI byte code' } | |
0x14c { 'x86' } | |
0x200 { 'Itanium 64 bit' } | |
0x9041 { 'Mitsubishi M32R little endian' } | |
0x266 { 'MIPS16' } | |
0x366 { 'MIPS with FPU' } | |
0x466 { 'MIPS16 with FPU' } | |
0x1f0 { 'PowerPC little endian' } | |
0x1f1 { 'PowerPC with floating point support' } | |
0x166 { 'MIPS little endian' } | |
0x1a2 { 'Hitachi SH3' } | |
0x1a3 { 'Hitachi SH3 DSP' } | |
0x1a6 { 'Hitachi SH4' } | |
0x1a8 { 'Hitachi SH5' } | |
0x1c2 { 'ARM or Thumb ("interworking")' } | |
0x169 { 'MIPS little endian WCE v2' } | |
default { | |
$NonStandardExeFound = $true | |
"{0:X0}" -f $RawMachineType | |
} | |
} | |
$Output = New-Object PSCustomObject | |
if($IncludeFileName) | |
{ | |
Add-Member -InputObject $Output -MemberType NoteProperty -Name Path -Value $Path | |
} | |
Add-Member -InputObject $Output -MemberType NoteProperty -Name TargetMachine -Value $TargetMachine | |
$Output | |
} | |
catch | |
{ | |
# the real purpose of the outer try/catch is to ensure that any file streams are properly closed. pass errors through | |
Write-Error $_ | |
} | |
finally | |
{ | |
if($FileStream) | |
{ | |
$FileStream.Close() | |
} | |
} | |
} | |
END { | |
if($NonStandardExeFound) | |
{ | |
Write-Warning -Message "Executable found with an unknown target machine type. Please refer to section 2.3.1 of the Microsoft documentation (http://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx)." | |
} | |
} | |
} | |
function WriteLog { | |
param ( | |
[switch] $Debug, | |
[switch] $Verbose, | |
[String] $Message, | |
[String] $SetPersistentLogPath, | |
[ValidateSet('DebugAndVerboseToLog','NoLogging','VerboseToLog')][string[]]$SetPersistentLogToFileLevel | |
) | |
if (!$PSScriptRoot) { | |
Write-Warning 'Can not create log, script has not been saved.' | |
throw | |
} | |
if ($Global:WriteLogDebugToLog -eq $null) { | |
Write-Debug '$Global:WriteLogDebugToLog is $null.' | |
if ([string]::IsNullOrEmpty($SetPersistentLogToFileLevel)) { | |
$Global:WriteLogDebugToLog = 'VerboseToLog' | |
} | |
else { | |
$Global:WriteLogDebugToLog = $SetPersistentLogToFileLevel | |
} | |
} | |
if ($SetPersistentLogToFileLevel -ne $null) { | |
if ($Global:WriteLogDebugToLog -eq $SetPersistentLogToFileLevel) { | |
Write-Debug '$Global:WriteLogDebugToLog is the same as $SetPersistentLogToFileLevel, no action required.' | |
} | |
else { | |
$Global:WriteLogDebugToLog = $SetPersistentLogToFileLevel | |
Write-Warning "Persistent log level previously set." | |
Write-Warning "Now overriding log level to $Global:WriteLogDebugToLog." | |
} | |
} | |
If ($Global:WriteLogPersistentPath -eq $null) { | |
Write-Debug '$Global:WriteLogPersistentPath is $null.' | |
If ([string]::IsNullOrEmpty($SetPersistentLogPath)) { | |
Write-Debug '$SetPersistentLogPath is $null.' | |
$Global:WriteLogPersistentPath = ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) + '.log' | |
} | |
else { | |
if (Test-Path $SetPersistentLogPath) { | |
Write-Debug '$SetPersistentLogPath is a valid path.' | |
if ((Get-Item $SetPersistentLogPath) -is [System.IO.DirectoryInfo]) { | |
Write-Debug '$SetPersistentLogPath is a directory.' | |
If ($SetPersistentLogPath -match '\\$') { | |
Write-Debug '$SetPersistentLogPath has a trailing backslash.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log' | |
} | |
else { | |
Write-Debug '$SetPersistentLogPath does not have a trailing backslash.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath + '\' + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log' | |
} | |
} | |
else { | |
Write-Debug '$SetPersistentLogPath is a path an existing file.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath | |
} | |
} | |
else { | |
If (Test-Path (Split-Path $SetPersistentLogPath)) { | |
Write-Debug '$SetPersistentLogPath is a directory path, creating log file on write.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath | |
} | |
else { | |
Write-Warning '$SetPersistentLogPath is not a valid directory.' | |
} | |
} | |
} | |
} | |
elseif (!([string]::IsNullOrEmpty($SetPersistentLogPath))) { | |
if (Test-Path $SetPersistentLogPath) { | |
Write-Debug '$SetPersistentLogPath is a valid path.' | |
if ((Get-Item $SetPersistentLogPath) -is [System.IO.DirectoryInfo]) { | |
Write-Debug '$SetPersistentLogPath is a directory.' | |
If ($SetPersistentLogPath -match '\\$') { | |
Write-Debug '$SetPersistentLogPath has a trailing backslash.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log' | |
} | |
else { | |
Write-Debug '$SetPersistentLogPath does not have a trailing backslash.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath + '\' + (Split-Path ((Get-PSCallStack | Where-Object ScriptName -ne $null)[-1].ScriptName) -Leaf) + '.log' | |
} | |
} | |
else { | |
Write-Debug '$SetPersistentLogPath is a path an existing file.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath | |
} | |
Write-Warning "Persistent log path previously set." | |
Write-Warning "Now overriding log path to $Global:WriteLogPersistentPath" | |
} | |
else { | |
If (Test-Path (Split-Path $SetPersistentLogPath)) { | |
Write-Debug '$SetPersistentLogPath is a directory path, creating log file on write.' | |
$Global:WriteLogPersistentPath = $SetPersistentLogPath | |
Write-Warning "Persistent log path previously set." | |
Write-Warning "Now overriding log path to $Global:WriteLogPersistentPath." | |
} | |
else { | |
Write-Warning '$SetPersistentLogPath is not a valid directory.' | |
} | |
} | |
} | |
if ($Debug) { | |
if (($Global:WriteLogDebugToLog -match 'DebugAndVerboseToLog')) { | |
"$(Get-Date -Format 'u') - Debug - $Message" | Out-File -Append $Global:WriteLogPersistentPath | |
} | |
if ($DebugPreference -eq 'Continue') { | |
Write-Debug $Message | |
} | |
} | |
if ($Verbose) { | |
if (($Global:WriteLogDebugToLog -match 'DebugAndVerboseToLog|VerboseToLog')) { | |
"$(Get-Date -Format 'u') - Verbose - $Message" | Out-File -Append $Global:WriteLogPersistentPath | |
} | |
if ($VerbosePreference -eq 'Continue') { | |
Write-Verbose $Message | |
} | |
} | |
} | |
#<<< End Of Function List >>> | |
# Setting up housekeeping ######################################################################### | |
$DebugPreference = 'SilentlyContinue' #SilentlyContinue|Continue | |
$VerbosePreference = 'SilentlyContinue' #SilentlyContinue|Continue | |
$LogPath = 'C:\Windows\Logs\Remediate-ExecutablesWithCVE.log' #$PSScriptRoot|C:\Windows\Logs | |
$LogLevel = 'VerboseToLog' #'NoLogging'|'VerboseToLog'|'DebugAndVerboseToLog' | |
$TestMode = $false | |
#<<< End of Setting up housekeeping >>> | |
# Start of script work ############################################################################ | |
WriteLog -SetPersistentLogPath $LogPath | |
WriteLog -SetPersistentLogToFileLevel $LogLevel | |
WriteLog -Verbose "++++++++++ STARTING COMPLIANCE ITEM SCRIPT ++++++++++" | |
WriteLog -Verbose "Loading CVE minimum versions." | |
$CVEMinimumVersionData= @' | |
FNPLicensingService.exe,11.16.1.0,x86,"\\Network\Share\FNPLicensingService x86 11.16.6\FNPLicensingService.exe" | |
FNPLicensingService64.exe,11.16.1.0,x64,"\\Network\Share\FNPLicensingService x64 11.16.4\fnplicensingservice64.exe" | |
lmgrd.exe,11.16.1.0,x86,"\\Network\Share\lmgrd x86 11.16.2\lmgrd.exe" | |
lmgrd.exe,11.16.1.0,x64,"\\Network\Share\lmgrd x64 11.16.2\lmgrd.exe" | |
'@ -split "`n" | ForEach-Object {$_.trim()} | |
$CVEMinimumVersion = @() | |
Foreach ($Line in $CVEMinimumVersionData) | |
{ | |
[array]$LineData = $Line.Split(",") | |
$KeyData = New-Object PSObject | |
$KeyData | Add-Member -MemberType NoteProperty -Name "File" -Value $LineData[0] | |
$KeyData | Add-Member -MemberType NoteProperty -Name "MinimumVersion" -Value $LineData[1] | |
$KeyData | Add-Member -MemberType NoteProperty -Name "Architecture" -Value $LineData[2] | |
$KeyData | Add-Member -MemberType NoteProperty -Name "ReplacementExePath" -Value $LineData[3] | |
$CVEMinimumVersion += $KeyData | |
} | |
WriteLog -Debug $($CVEMinimumVersion | Out-String) | |
WriteLog -Verbose "Loading CVE known file locations." | |
$CVEKnownFileLocations = @( | |
'C:\Program Files (x86)\Common Files\Macrovision Shared\FlexNet Publisher\FNPLicensingService.exe' | |
'C:\Program Files\Common Files\Macrovision Shared\FlexNet Publisher\FNPLicensingService64.exe' | |
'C:\Program Files (x86)\Rockwell Software\FactoryTalk Activation\lmgrd.exe' | |
'C:\Program Files\MATLAB\R2019a\etc\win64\lmgrd.exe' | |
'C:\Non-existent Path\Non-existent.exe' | |
) | |
$CVEKnownFileLocations | ForEach-Object { | |
$File = $_ | |
WriteLog -Verbose "Start work on $File." | |
If (Test-Path $File) { | |
$File = Get-Item $File | |
$FileVersion = ("{0}.{1}.{2}.{3}" -f ($File.VersionInfo).FileMajorPart, ($File.VersionInfo).FileMinorPart, ($File.VersionInfo).FileBuildPart, ($File.VersionInfo).FilePrivatePart) | |
WriteLog -Verbose "File found: $($File.Name) $FileVersion." | |
$Element = @() | |
$Element += ($CVEMinimumVersion.File | Select-String "$($File.Name)").LineNumber | ForEach-Object {$_ - 1} | |
if ($Element.Count -gt 1) { | |
WriteLog -Debug "Two or more elements found in array for file name, due to same file name but diffent architecture." | |
$FileArchitecture = (Get-ExeTargetMachine $File.FullName).TargetMachine | |
$Element | ForEach-Object { | |
If ($CVEMinimumVersion[($_)] -match "$FileArchitecture") { | |
$Element = $_ | |
} | |
} | |
} | |
WriteLog -Verbose "`$CVEMinimumVersionData element required is $Element. " | |
WriteLog -Verbose "Raw element data: $($CVEMinimumVersion[$Element])" | |
WriteLog -Verbose 'Preforming version checks with element data.' | |
if ($FileVersion -gt $CVEMinimumVersion.MinimumVersion[$Element]) { | |
WriteLog -Verbose 'File is not vulnerable, remediation is not required.' | |
} | |
else { | |
WriteLog -Verbose 'Remediation required.' | |
$Services = Get-WmiObject win32_service | select Name, DisplayName, State, PathName | |
$ServicesName = $null | |
if (!($($Services.PathName | Select-String $([Regex]::Escape($File.FullName))) -eq $null)) { | |
$ServicesName = $($Services[($Services.PathName | Select-String $([Regex]::Escape($File.FullName))).LineNumber -1]).Name | |
WriteLog -Verbose 'A Windows Service has been found with the same working path. ' | |
WriteLog -Verbose "Service name: $ServicesName." | |
} | |
else { | |
WriteLog -Verbose 'No Windows Service found.' | |
} | |
WriteLog -Debug "$File.FullName: $($File.FullName)" | |
WriteLog -Debug "`$ServicesName: $ServicesName" | |
If (!($ServicesName -eq $null)) { | |
WriteLog -Verbose "Stopping '$ServicesName' service." | |
Stop-Service -Name $ServicesName -WarningAction SilentlyContinue | |
} | |
Rename-Item -Path $File.FullName -NewName $($File.Name + '.BACKUP') -WhatIf:$TestMode | |
WriteLog -Verbose "Renamed file success: $?" | |
if (Test-Path $($CVEMinimumVersion.ReplacementExePath[$Element] -replace '"')) { | |
Copy-Item -Path $($CVEMinimumVersion.ReplacementExePath[$Element] -replace '"') -Destination $File.FullName -WhatIf:$TestMode | |
WriteLog -Verbose "File replacement success: $?" | |
WriteLog -Verbose 'WARNING: File was vulnerable and was replaced!' | |
"Executable was replaced by MECM Configuration Item due to Common Vulnerabilities and Exposures (CVE)." | Out-File $($File.FullName + '.BACKUP.ReadMe') | |
} | |
else { | |
Rename-Item -Path $($File.FullName + '.BACKUP') -NewName $File.Name -WhatIf:$TestMode | |
WriteLog -Verbose 'Error, copy of replacement file failed because Test-Path failed.' | |
} | |
If (!($ServicesName -eq $null)) { | |
WriteLog -Verbose "Starting '$ServicesName' service." | |
Start-Service -Name $ServicesName -WarningAction SilentlyContinue | |
} | |
} | |
} | |
else { | |
WriteLog -Verbose 'File not found.' | |
} | |
} | |
If ($Error.Count -eq 0) { | |
WriteLog -Verbose "Completed with return code: 0." | |
return 0 | |
} | |
else { | |
WriteLog -Verbose "Completed with the following error." | |
WriteLog -Verbose $($Error | Out-String) | |
return $Error | |
} | |
#<<< End of script work >>> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment