Apply CVE-2023-24932 SecureBoot Update to Endpoints via Configuration Manager Application

5/25/2003 Update: Thank you to Curt Ricard for the tweak to make the file copy behave better.

The May 2023 Windows update release contains an update to SecureBoot DBX. The fixes require steps to implement beyond just installing the monthly update. In July 2023, there will be a second version that makes the SecureBoot changes easier to apply. In early 2024 the SecureBoot updates will be forced.

For a security issue, I do not want to wait for the fix to be done for me, I want it done ASAP. The official MSFT guidance has steps to apply the fixes. These are great, but I want to automate them. There are three phases to the fixes. Phases 1 & 2 can be done side by side without any adverse affects. Phase 3 should not be done until phase 2 is complete. Phase 3 can’t be applied to an endpoint until phase 1 is done on that specific endpoint.

Phase 1: Apply May 2023 updates to endpoints
Phase 2: Update boot images to contain the new SecureBoot file (see Gary Blok’s post)
Phase 3: Apply the SecureBoot change to individual endpoints

Phase 3 is the focus of this post.

There are two steps to accomplish on each endpoint. Copy the updated signature file to the EFI partition and set a registry key to tell Windows to load the updated signatures on reboot. After the first reboot, Windows will process the updated signatures and apply them to the system hardware. Sounds easy enough to accomplish if just thinking about the basic tasks, we all know plenty of methods to copy files and set registry values. We are not going to have any revolutionary methods to do that here.

What I am more interested in is how do I mix the automation of these tasks with monitoring/reporting? A Configuration Manager Application of course!

The first consideration is how do we make sure that the application can only run on systems that already have the May update installed. A simple Global Condition can handle this, checking that the DBXUpdateKB.bin file exists.

Next is thinking about a detection method. How can we determine that the SecureBoot fix has been applied to the system? For that I found scripts to check a bin file against the system to see if the changes have been applied. These scripts are available here. There is a slight catch. The DBXUpdateKB.bin file is both a payload and a signature. We need to split it into just the payload to use it for scanning. For that I found yet another script to split the file, available here. I don’t want to have to send every endpoint off to pull scripts from here and there, or even include all the scripts as files in my application. Instead, I converted them into functions and included them in the scripts below. The detection method looks at the extracted bin file and verifies it has been applied to the local system.

function Get-UefiDatabaseSignatures {
<#
.SYNOPSIS

Parses UEFI Signature Databases into logical Powershell objects

.DESCRIPTION

Original Author: Matthew Graeber (@mattifestation)
Modified By: Jeremiah Cox (@int0x6)
Additional Source:  https://gist.github.com/mattifestation/991a0bea355ec1dc19402cef1b0e3b6f
License: BSD 3-Clause

.PARAMETER Variable

Specifies a UEFI variable, an instance of which is returned by calling the Get-SecureBootUEFI cmdlet. Only 'db' and 'dbx' are supported.

.PARAMETER BytesIn

Specifies a byte array consisting of the PK, KEK, db, or dbx UEFI vairable contents.

.EXAMPLE

$DbxBytes = [IO.File]::ReadAllBytes('.\dbx.bin')

Get-UEFIDatabaseSignatures -BytesIn $DbxBytes

.EXAMPLE

Get-SecureBootUEFI -Name db | Get-UEFIDatabaseSignatures

.EXAMPLE

Get-SecureBootUEFI -Name dbx | Get-UEFIDatabaseSignatures

.EXAMPLE

Get-SecureBootUEFI -Name pk | Get-UEFIDatabaseSignatures

.EXAMPLE

Get-SecureBootUEFI -Name kek | Get-UEFIDatabaseSignatures

.INPUTS

Microsoft.SecureBoot.Commands.UEFIEnvironmentVariable

Accepts the output of Get-SecureBootUEFI over the pipeline.

.OUTPUTS

UefiSignatureDatabase

Outputs an array of custom powershell objects describing a UEFI Signature Database. "77fa9abd-0359-4d32-bd60-28f4e78f784b" refers to Microsoft as the owner.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'UEFIVariable')]
        [ValidateScript({ ($_.GetType().Fullname -eq 'Microsoft.SecureBoot.Commands.UEFIEnvironmentVariable') -and (($_.Name -eq 'kek') -or ($_.Name -eq 'pk') -or ($_.Name -eq 'db') -or ($_.Name -eq 'dbx')) })]
        $Variable,

        [Parameter(Mandatory, ParameterSetName = 'ByteArray')]
        [Byte[]]
        [ValidateNotNullOrEmpty()]
        $BytesIn
    )

    $SignatureTypeMapping = @{
        'C1C41626-504C-4092-ACA9-41F936934328' = 'EFI_CERT_SHA256_GUID' # Most often used for dbx
        'A5C059A1-94E4-4AA7-87B5-AB155C2BF072' = 'EFI_CERT_X509_GUID'   # Most often used for db
    }

    $Bytes = $null

    if ($Variable) {
        $Bytes = $Variable.Bytes
    } else {
        $Bytes = $BytesIn
    }

    try {
        $MemoryStream = New-Object -TypeName IO.MemoryStream -ArgumentList @(,$Bytes)
        $BinaryReader = New-Object -TypeName IO.BinaryReader -ArgumentList $MemoryStream, ([Text.Encoding]::Unicode)
    } catch {
        throw $_
        return
    }

    # What follows will be an array of EFI_SIGNATURE_LIST structs

    while ($BinaryReader.PeekChar() -ne -1) {
        $SignatureType = $SignatureTypeMapping[([Guid][Byte[]] $BinaryReader.ReadBytes(16)).Guid]
        $SignatureListSize = $BinaryReader.ReadUInt32()
        $SignatureHeaderSize = $BinaryReader.ReadUInt32()
        $SignatureSize = $BinaryReader.ReadUInt32()

        $SignatureHeader = $BinaryReader.ReadBytes($SignatureHeaderSize)

        # 0x1C is the size of the EFI_SIGNATURE_LIST header
        $SignatureCount = ($SignatureListSize - 0x1C) / $SignatureSize

        $SignatureList = 1..$SignatureCount | ForEach-Object {
            $SignatureDataBytes = $BinaryReader.ReadBytes($SignatureSize)

            $SignatureOwner = [Guid][Byte[]] $SignatureDataBytes[0..15]

            switch ($SignatureType) {
                'EFI_CERT_SHA256_GUID' {
                    $SignatureData = ([Byte[]] $SignatureDataBytes[0x10..0x2F] | ForEach-Object { $_.ToString('X2') }) -join ''
                }

                'EFI_CERT_X509_GUID' {
                    $SignatureData = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,([Byte[]] $SignatureDataBytes[16..($SignatureDataBytes.Count - 1)]))
                }
            }

            [PSCustomObject] @{
                PSTypeName = 'EFI.SignatureData'
                SignatureOwner = $SignatureOwner
                SignatureData = $SignatureData
            }
        }

        [PSCustomObject] @{
            PSTypeName = 'EFI.SignatureList'
            SignatureType = $SignatureType
            SignatureList = $SignatureList
        }
    }
}

#Write-Host "Checking for Administrator permission..."
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
  Write-Warning "Insufficient permissions to run this script. Open the PowerShell console as administrator and run this script again."
  Break
} else {
  #Write-Host "Running as administrator � continuing execution..." -ForegroundColor Green
}

 $patchfile  = "$($ENV:SystemRoot)\System32\May2023SecureBootUpdate\content.bin"

 if (test-path $patchfile) {

     $patchfile = (gci $patchfile).FullName

     # Print computer info
     $computer = gwmi Win32_ComputerSystem
     $bios = gwmi Win32_BIOS
     #"Manufacturer: " + $computer.Manufacturer
     #"Model: " + $computer.Model
     $biosinfo = $bios.Manufacturer , $bios.Name , $bios.SMBIOSBIOSVersion , $bios.Version -join ", "
     #"BIOS: " + $biosinfo + "`n"

     $DbxRaw = Get-SecureBootUEFI dbx
     $DbxFound = $DbxRaw | Get-UEFIDatabaseSignatures

     $DbxBytesRequired = [IO.File]::ReadAllBytes($patchfile)
     $DbxRequired = Get-UEFIDatabaseSignatures -BytesIn $DbxBytesRequired

     # Flatten into an array of required EfiSignatureData data objects
     $RequiredArray = foreach ($EfiSignatureList in $DbxRequired) {
         Write-Verbose $EfiSignatureList
         foreach ($RequiredSignatureData in $EfiSignatureList.SignatureList) {
             Write-Verbose  $RequiredSignatureData
             $RequiredSignatureData.SignatureData
         }
     }
     Write-Information "Required `n" $RequiredArray

     # Flatten into an array of EfiSignatureData data objects (read from dbx)
     $FoundArray = foreach ($EfiSignatureList in $DbxFound) {
         Write-Verbose $EfiSignatureList
         foreach ($FoundSignatureData in $EfiSignatureList.SignatureList) {
             Write-Verbose  $FoundSignatureData
             $FoundSignatureData.SignatureData
         }
     }
     Write-Information "Found `n" $FoundArray

     $successes = 0
     $failures = 0
     $requiredCount = $RequiredArray.Count
     foreach ($RequiredSig in $RequiredArray) {
        if ($FoundArray -contains $RequiredSig) {
            Write-Information "FOUND: $RequiredSig"
            $successes++
        } else {
            Write-Error "!!! NOT FOUND`n$RequiredSig`n!!!`n"
            $failures++
        }
        $i = $successes + $failures
        Write-Progress -Activity 'Checking if all patches applied' -Status "Checking element $i of $requiredCount" -PercentComplete ($i/$requiredCount *100)
     }

     if ($failures -ne 0) {
         #Write-Error "!!! FAIL:  $failures failures detected!"
         # $DbxRaw.Bytes | sc -encoding Byte dbx_found.bin
     } elseif ($successes -ne $RequiredArray.Count) {
         #Write-Error "!!! Unexpected: $successes != $requiredCount expected successes!"
     } elseif ($successes -eq 0) {
         #Write-Error "!!! Unexpected failure:  no successes detected, check command-line usage."
     } else {
         write-output "Installed"
     }
 }

You may have noticed that the detection method uses “$($ENV:SystemRoot)\System32\May2023SecureBootUpdate\content.bin” to scan against but we also need to make sure that exists. I added the function based on the db split script to my install script. First the function to so the split then a second function to handle the directory and calling the file split as required.

Function Split-DbxAuthInfo{
    <#PSScriptInfo
 
    .VERSION 1.0
 
    .GUID ec45a3fc-5e87-4d90-b55e-bdea083f732d
 
    .AUTHOR Microsoft Secure Boot Team
 
    .COMPANYNAME Microsoft
 
    .COPYRIGHT Microsoft
 
    .TAGS Windows Security
 
    .LICENSEURI
 
    .PROJECTURI
 
    .ICONURI
 
    .EXTERNALMODULEDEPENDENCIES
 
    .REQUIREDSCRIPTS
 
    .EXTERNALSCRIPTDEPENDENCIES
 
    .RELEASENOTES
    Version 1.0: Original published version.
 
    #>

    <#
    .DESCRIPTION
     Splits a DBX update package into the new DBX variable contents and the signature authorizing the change.
     To apply an update using the output files of this script, try:
     Set-SecureBootUefi -Name dbx -ContentFilePath .\content.bin -SignedFilePath .\signature.p7 -Time 2010-03-06T19:17:21Z -AppendWrite'
    .EXAMPLE
    .\SplitDbxAuthInfo.ps1 DbxUpdate_x64.bin
    #>

        [CmdletBinding()]
        param(
            [Parameter(Position = 0, Mandatory = $true)]$filepath,
            [Parameter(Position = 1, Mandatory = $true)]$OutputDir
        )

    # Get file from script input
    $file  = Get-Content -Encoding Byte $filepath

    $outputbin = "$($OutputDir)\content.bin"
    $outputp7 = "$($OutputDir)\signature.p7"

    # Identify file signature
    $chop = $file[40..($file.Length - 1)]
    if (($chop[0] -ne 0x30) -or ($chop[1] -ne 0x82 )) {
        Write-Error "Cannot find signature"
        exit 1
    }

    # Signature is known to be ASN size plus header of 4 bytes
    $sig_length = ($chop[2] * 256) + $chop[3] + 4
    $sig = $chop[0..($sig_length - 1)]

    if ($sig_length -gt ($file.Length + 40)) {
        Write-Error "Signature longer than file size!"
        #exit 1
    }

    # Build and write signature output file
    [System.Byte[]] $sigbytes =  @()
    foreach ($i in $sig) {$sigbytes += $i}
    Set-Content -Encoding Byte -Path $outputp7 -Value $sigbytes
    Write-verbose "Successfully created output file $outputp7"

    # Build and write variable content output file
    $content = $chop[$sig_length..($chop.Length - 1)]
    [System.Byte[]] $bytes =  @()
    foreach ($i in $content) {$bytes += $i}
    Set-Content -Encoding Byte -Path $outputbin -Value $bytes
    Write-Verbose "Successfully created output file $outputbin"
}

Function Check-ContentBin{
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true)]$filepath,
        [Parameter(Position = 1, Mandatory = $true)]$OutputDir
    )
    $outputbin = "$($OutputDir)\content.bin"
    if(!(test-path $outputbin)){
        if(!(test-path $OutputDir)){New-Item -Path $OutputDir -ItemType Directory| out-null}
        Split-DbxAuthInfo $filepath $OutputDir
    }
}

#verify the content.bin file is extracted for use in detection method
Check-ContentBin "$($env:SystemRoot)\System32\SecureBootUpdates\DBXUpdateKB.bin" "$($env:SystemRoot)\System32\May2023SecureBootUpdate"

Now that we have the content.bin extracted to the location where the detection method will look for it, let’s accomplish the work of applying the fix to the endpoint. The first step is a slightly modified version of the MSFT guidance. All that I added is a check to make sure the file has not already been copied.

mountvol q: /S
if(!(test-path "q:\EFI\Microsoft\Boot\SKUSiPolicy.p7b")){
    cmd.exe /c 'xcopy %systemroot%\System32\SecureBootUpdates\SKUSiPolicy.p7b q:\EFI\Microsoft\Boot'
}
mountvol q: /D

Then it is creating the registry value. I did convert this to a powershell cmdlet instead of the reg cmd as published in the guidance.

New-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\Secureboot" -Name AvailableUpdates -PropertyType DWord -Value 0x10 -force

Now that both changes are applied, we need the system to reboot. I’d prefer to do so gracefully and give users notice as defined in client settings. To do this, we force the script to exit with a 3010 exit code to tell Configuration Manager a reboot is required.

exit 3010

After the reboot, Windows will apply the update to the system firmware. The next reboot after this will make the changes active. The official guidance is to allow five minutes for this to happen and reboot a second time. I considered using a task sequence to handle this but in the end, I decided the second reboot can be handled by whatever method and schedule that is right for a given environment. I am not going to worry about it as part of the application.

Putting the install script together…

Function Split-DbxAuthInfo{
    <#PSScriptInfo
 
    .VERSION 1.0
 
    .GUID ec45a3fc-5e87-4d90-b55e-bdea083f732d
 
    .AUTHOR Microsoft Secure Boot Team
 
    .COMPANYNAME Microsoft
 
    .COPYRIGHT Microsoft
 
    .TAGS Windows Security
 
    .LICENSEURI
 
    .PROJECTURI
 
    .ICONURI
 
    .EXTERNALMODULEDEPENDENCIES
 
    .REQUIREDSCRIPTS
 
    .EXTERNALSCRIPTDEPENDENCIES
 
    .RELEASENOTES
    Version 1.0: Original published version.
 
    #>

    <#
    .DESCRIPTION
     Splits a DBX update package into the new DBX variable contents and the signature authorizing the change.
     To apply an update using the output files of this script, try:
     Set-SecureBootUefi -Name dbx -ContentFilePath .\content.bin -SignedFilePath .\signature.p7 -Time 2010-03-06T19:17:21Z -AppendWrite'
    .EXAMPLE
    .\SplitDbxAuthInfo.ps1 DbxUpdate_x64.bin
    #>

        [CmdletBinding()]
        param(
            [Parameter(Position = 0, Mandatory = $true)]$filepath,
            [Parameter(Position = 1, Mandatory = $true)]$OutputDir
        )

    # Get file from script input
    $file  = Get-Content -Encoding Byte $filepath

    $outputbin = "$($OutputDir)\content.bin"
    $outputp7 = "$($OutputDir)\signature.p7"

    # Identify file signature
    $chop = $file[40..($file.Length - 1)]
    if (($chop[0] -ne 0x30) -or ($chop[1] -ne 0x82 )) {
        Write-Error "Cannot find signature"
        exit 1
    }

    # Signature is known to be ASN size plus header of 4 bytes
    $sig_length = ($chop[2] * 256) + $chop[3] + 4
    $sig = $chop[0..($sig_length - 1)]

    if ($sig_length -gt ($file.Length + 40)) {
        Write-Error "Signature longer than file size!"
        #exit 1
    }

    # Build and write signature output file
    [System.Byte[]] $sigbytes =  @()
    foreach ($i in $sig) {$sigbytes += $i}
    Set-Content -Encoding Byte -Path $outputp7 -Value $sigbytes
    Write-verbose "Successfully created output file $outputp7"

    # Build and write variable content output file
    $content = $chop[$sig_length..($chop.Length - 1)]
    [System.Byte[]] $bytes =  @()
    foreach ($i in $content) {$bytes += $i}
    Set-Content -Encoding Byte -Path $outputbin -Value $bytes
    Write-Verbose "Successfully created output file $outputbin"
}

Function Check-ContentBin{
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true)]$filepath,
        [Parameter(Position = 1, Mandatory = $true)]$OutputDir
    )
    $outputbin = "$($OutputDir)\content.bin"
    if(!(test-path $outputbin)){
        if(!(test-path $OutputDir)){New-Item -Path $OutputDir -ItemType Directory| out-null}
        Split-DbxAuthInfo $filepath $OutputDir
    }
}

#verify the content.bin file is extracted for use in detection method
Check-ContentBin "$($env:SystemRoot)\System32\SecureBootUpdates\DBXUpdateKB.bin" "$($env:SystemRoot)\System32\May2023SecureBootUpdate"

#copy the update SecureBoot file to the EFI partition
mountvol q: /S
if(!(test-path "q:\EFI\Microsoft\Boot\SKUSiPolicy.p7b")){
    cmd.exe /c 'xcopy %systemroot%\System32\SecureBootUpdates\SKUSiPolicy.p7b q:\EFI\Microsoft\Boot'
}
mountvol q: /D

#set Reg key to tell system to load update UEFI signatures on reboot
New-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\Secureboot" -Name AvailableUpdates -PropertyType DWord -Value 0x10 -force

#pass exit code 3010 to tell CM to do a soft reboot
exit 3010

If you would like to manually create the application based on the above, you are free to do so. If you’d prefer to import an already created version, it is linked below. The exported application does not contain the install script, you will need to copy that from above and set the application to the location you save it.