Skip to content

Instantly share code, notes, and snippets.

@rileyz
Last active August 19, 2020 10:08
Show Gist options
  • Save rileyz/35d6390a7f23b514f0c16e829ed07db5 to your computer and use it in GitHub Desktop.
Save rileyz/35d6390a7f23b514f0c16e829ed07db5 to your computer and use it in GitHub Desktop.
<#
.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