diff --git a/README.md b/README.md index f82d27b..f5082f4 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,30 @@ Example output: Screenshot +## Audit ESP or an attached drive for revoked EFI binaries (DBX) + +Right-click `Scan ESP for revoked files.cmd` and select *Run as administrator*. + +This script: +- Downloads the latest Microsoft DBX JSON +- Scans EFI binaries in the ESP +- Matches them against revoked hashes and certificates + +You can also scan other drives (e.g., USB, CD-ROM): + +`powershell -ExecutionPolicy Bypass -Command "& 'ps\Find-EfiFilesRevokedByDbx.ps1' -Paths D:\ -MatchMode Both -MsftJsonPath C:\path\dbx_info_msft_latest.json -ScanESP:$false` + +Default JSON: +https://raw.githubusercontent.com/microsoft/secureboot_objects/main/PreSignedObjects/DBX/dbx_info_msft_latest.json + +Example output: +DBX audit scan output + +[!WARNING] +Detection is based on hash and certificate matching only. +Newer revocations using **SVN (version-based enforcement)** and **SBAT** are **not currently checked**. However `Check EFI file info.cmd` will display SVN/SBAT data if present, but this tool currently does not compare it against UEFI NVRAM policy. Support for SVN and SBAT comparison is welcome as a feature request. + + ## Re-applying the Secure Boot DBX updates If the Secure Boot variables were accidentally reset to default in the UEFI/BIOS settings for example, it is possible to make Windows re-apply the DBX updates that Windows had previously applied. Right-click `Apply DBX update.cmd` and *Run as administrator*. Wait for awhile. The DBX updates should be applied after that. diff --git a/Scan ESP for revoked files.cmd b/Scan ESP for revoked files.cmd new file mode 100644 index 0000000..6b83f35 --- /dev/null +++ b/Scan ESP for revoked files.cmd @@ -0,0 +1,11 @@ +:: Created for cjee21/Check-UEFISecureBootVariables +@echo off +title Scan EFI files against Microsoft DBX JSON + +:: NOTE: Replace URL with a raw URL or use -MsftJsonPath to a local file. +set MSFT_JSON_URL=https://raw.githubusercontent.com/microsoft/secureboot_objects/main/PreSignedObjects/DBX/dbx_info_msft_latest.json + +powershell -ExecutionPolicy Bypass -Command "& '%~dp0ps\Find-EfiFilesRevokedByDbx.ps1' -ScanESP -ScanDefaultPaths -MatchMode MsftJson -MsftJsonUrl '%MSFT_JSON_URL%'" + +echo. +pause \ No newline at end of file diff --git a/docs/screenshot-audit.png b/docs/screenshot-audit.png new file mode 100644 index 0000000..f4d4f42 Binary files /dev/null and b/docs/screenshot-audit.png differ diff --git a/ps/Find-EfiFilesRevokedByDbx.ps1 b/ps/Find-EfiFilesRevokedByDbx.ps1 new file mode 100644 index 0000000..8b6dcf5 --- /dev/null +++ b/ps/Find-EfiFilesRevokedByDbx.ps1 @@ -0,0 +1,258 @@ +# Created for cjee21/Check-UEFISecureBootVariables +# Purpose: Walk EFI binaries and warn if they match revocations via: +# - file hash (Authenticode hash if you wire it in later) +# - signer certificate match (X509 DER match) against current DBX (EFI_CERT_X509_GUID) +# Also supports checking against microsoft/secureboot_objects dbx_info_msft_latest.json. + +[CmdletBinding()] +param( + # Root directories to scan for .efi files (optional) + [string[]] $Paths, + + # If set, will mount ESP to S: (mountvol s: /s) and scan it (default: true) + [switch] $ScanESP = $true, + + # Helper flag: scan common OS paths too (default: false) + [switch] $ScanDefaultPaths = $false, + + # Which revocation source(s) to match against + [ValidateSet('CurrentDbx','MsftJson','Both')] + [string] $MatchMode = 'CurrentDbx', + + # Optional: local path to dbx_info_msft_latest.json + [string] $MsftJsonPath, + + # Optional: URL to download dbx_info_msft_latest.json + [string] $MsftJsonUrl +) + +$ErrorActionPreference = 'Stop' + +Import-Module "$PSScriptRoot\Get-UEFIDatabaseSignatures.ps1" -Force +Import-Module "$PSScriptRoot\Get-EfiSignatures.ps1" -Force + +function Get-FileSha256Hex { + param([Parameter(Mandatory)][string] $FilePath) + (Get-FileHash -Algorithm SHA256 -LiteralPath $FilePath).Hash.ToUpperInvariant() +} + +function Get-CertDerHex { + param([Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert) + ([System.BitConverter]::ToString($Cert.RawData) -replace '-', '').ToUpperInvariant() +} + +function Get-DbxSetsFromCurrentDbx { + # Returns: + # - HashSet of SHA256 hex strings (EFI_CERT_SHA256_GUID) + # - HashSet of X509 DER hex strings (EFI_CERT_X509_GUID) + $dbx = Get-SecureBootUEFI -Name dbx + $parsed = $dbx | Get-UEFIDatabaseSignatures + + $sha256Set = New-Object 'System.Collections.Generic.HashSet[string]' + $x509DerSet = New-Object 'System.Collections.Generic.HashSet[string]' + + foreach ($list in $parsed) { + foreach ($entry in $list.SignatureList) { + if ($list.SignatureType -eq 'EFI_CERT_SHA256_GUID') { + [void]$sha256Set.Add(($entry.SignatureData.ToString().ToUpperInvariant())) + } elseif ($list.SignatureType -eq 'EFI_CERT_X509_GUID') { + $derHex = Get-CertDerHex -Cert $entry.SignatureData + [void]$x509DerSet.Add($derHex) + } + } + } + + [PSCustomObject]@{ + Name = 'CurrentDbx' + Sha256Set = $sha256Set + X509DerSet = $x509DerSet + } +} + +function Get-DbxSetsFromMsftJson { + param( + [string] $Path, + [string] $Url + ) + + $jsonText = $null + if ($Path) { + if (-not (Test-Path -LiteralPath $Path)) { + throw "MsftJsonPath not found: $Path" + } + $jsonText = Get-Content -LiteralPath $Path -Raw + } elseif ($Url) { + # Download to memory + $jsonText = (Invoke-WebRequest -UseBasicParsing -Uri $Url).Content + } else { + throw "MatchMode requires -MsftJsonPath or -MsftJsonUrl." + } + + $j = $jsonText | ConvertFrom-Json + + # MSFT JSON structure: { "images": { "x64": [ { authenticodeHash, flatHash, ... }, ... ], "arm64": [ ... ] } } + $sha256Set = New-Object 'System.Collections.Generic.HashSet[string]' + + foreach ($archProp in $j.images.PSObject.Properties) { + $archName = $archProp.Name + $items = $archProp.Value + foreach ($img in $items) { + if ($img.authenticodeHash -and $img.authenticodeHash.Trim()) { + #Focus on authenticodeHash for matches as most reliable + [void]$sha256Set.Add($img.authenticodeHash.Trim().ToUpperInvariant()) + } + } + } + + # MSFT JSON does not directly provide DER blobs for revoked cert entries (at least in the snippet provided), + # so in MsftJson mode we can only do hash-based checks unless you add a mapping of signer certs separately. + $x509DerSet = New-Object 'System.Collections.Generic.HashSet[string]' +if ($j.PSObject.Properties.Name -contains 'certificates' -and $j.certificates) { + foreach ($cert in $j.certificates) { + $tp = $cert.thumbprint + if ($tp -and $tp.Trim()) { + # normalize: remove separators/spaces just in case, and normalize case + $norm = ($tp.Trim() -replace '[^0-9a-fA-F]', '').ToUpperInvariant() + + # optional sanity check: SHA1 thumbprints are 20 bytes => 40 hex chars + if ($norm.Length -eq 40) { + [void]$x509DerSet.Add($norm) + } + } + } +} + + [PSCustomObject]@{ + Name = 'MsftJson' + Sha256Set = $sha256Set + X509DerSet = $x509DerSet + } +} + +function Get-EfiFilesUnderPaths { + param([Parameter(Mandatory)][string[]] $RootPaths) + + $all = New-Object System.Collections.Generic.List[string] + foreach ($root in $RootPaths) { + if (-not (Test-Path -LiteralPath $root)) { continue } + Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.Extension -ieq '.efi' } | + ForEach-Object { $all.Add($_.FullName) | Out-Null } + } + $all +} + +# Build scan roots +$scanRoots = New-Object System.Collections.Generic.List[string] + +$didMountEsp = $false +if ($ScanESP) { + try { + mountvol s: /s | Out-Null + $didMountEsp = $true + $scanRoots.Add('S:\') | Out-Null + } catch { + Write-Warning "Could not mount ESP to S:. Run as Administrator? Continuing..." + } +} + +if ($ScanDefaultPaths) { + # Common locations where EFI binaries may exist on the OS volume. + $scanRoots.Add("$env:SystemRoot\Boot\EFI") | Out-Null + $scanRoots.Add("$env:SystemDrive\EFI") | Out-Null + $scanRoots.Add("$env:SystemDrive\Boot") | Out-Null +} + +if ($Paths) { + foreach ($p in $Paths) { $scanRoots.Add($p) | Out-Null } +} + +if ($scanRoots.Count -eq 0) { + throw "No scan roots specified and ESP mount failed. Provide -Paths or run elevated." +} + +# Load revocation sets +$revocationSets = New-Object System.Collections.Generic.List[object] + +if ($MatchMode -eq 'CurrentDbx' -or $MatchMode -eq 'Both') { + Write-Host "Loading current DBX..." -ForegroundColor Cyan + $revocationSets.Add((Get-DbxSetsFromCurrentDbx)) | Out-Null +} + +if ($MatchMode -eq 'MsftJson' -or $MatchMode -eq 'Both') { + Write-Host "Loading Microsoft DBX JSON..." -ForegroundColor Cyan + $revocationSets.Add((Get-DbxSetsFromMsftJson -Path $MsftJsonPath -Url $MsftJsonUrl)) | Out-Null +} + +foreach ($s in $revocationSets) { + Write-Host ("Loaded {0}: SHA256-like={1}, X509={2}" -f $s.Name, $s.Sha256Set.Count, $s.X509DerSet.Count) +} + +Write-Host "Scanning for EFI binaries..." -ForegroundColor Cyan +$efiFiles = Get-EfiFilesUnderPaths -RootPaths $scanRoots.ToArray() +Write-Host ("Found {0} EFI file(s)." -f $efiFiles.Count) + +$warnCount = 0 +$idx = 0 + +foreach ($file in $efiFiles) { + $idx++ + Write-Progress -Activity "Checking EFI files" -Status $file -PercentComplete (($idx / [Math]::Max(1, $efiFiles.Count)) * 100) + + $fileSha = $null + + # Signer cert DER hexes (may be empty) + $signerThumbprints = @() + try { + $sigs = Get-EfiSignatures -FilePath $file + $fileSha = $sigs.Authentihash + foreach ($sig in $sigs.Signatures) { + foreach ($c in $sig.Certificates) { + if ($c -and $c.Thumbprint) { + $signerThumbprints += $c.Thumbprint.ToUpperInvariant() + } + } + } + } catch {} + + $matches = @() + + foreach ($set in $revocationSets) { + # Hash match + if ($fileSha -and $set.Sha256Set.Contains($fileSha)) { + $matches += [PSCustomObject]@{ Source=$set.Name; Type='Hash'; Detail='SHA256(Authenticode) matches revocation list' } + } + + # Cert match (only meaningful for CurrentDbx unless you add cert data to MsftJson mode) + if ($signerThumbprints.Count -gt 0 -$set.X509DerSet.Count -gt 0) { + foreach ($derHex in $signerThumbprints) { + if ($set.X509DerSet.Contains($derHex)) { + $matches += [PSCustomObject]@{ Source=$set.Name; Type='SignerCert'; Detail='Signer certificate DER matches DBX X509 revocation' } + break + } + } + } + } + + if ($matches.Count -gt 0) { + $warnCount++ + Write-Host "" + Write-Host "WARNING: EFI file matches revocation list(s)" -ForegroundColor Yellow + Write-Host (" Path: {0}" -f $file) + if ($fileSha) { Write-Host (" SHA256 (Authenticode): {0}" -f $fileSha) } + if ($signerThumbprints.Count -gt 0) { + Write-Host (" Signer thumbprint(s): {0}" -f ($signerThumbprints -join ', ')) + } + + foreach ($m in $matches) { + Write-Host (" Match: [{0}] {1} - {2}" -f $m.Source, $m.Type, $m.Detail) -ForegroundColor Yellow + } + } +} + +Write-Host "" +Write-Host ("Scan complete. Warnings: {0}" -f $warnCount) -ForegroundColor Cyan + +if ($didMountEsp) { + try { mountvol s: /d | Out-Null } catch {} +} \ No newline at end of file