In late July 2024 Ars Technica wrote an article about broken Secure Boot and a UEFI (i.e., the BIOS successor) key implementation failure. Binarly REsearch discovered a Secure Boot master key, called the Platform Key (PK), that had been published with weak protections on github. They call this PKfail. This was only supposed to be a test PK that should have been replaced by OEM vendors. The PK even included certificate signatures with an Issuer name of “DO NOT TRUST” or “DO NOT SHIP.” However, this PK was widely distributed in production systems from some major manufacturers. The ramifications of this are a compromise of the UEFI ecosystem and allows complete bypass of Secure Boot. An attacker with local admin can then boot to anything of their choosing as they can manipulate the other trusted components stored in the firmware nonvolatile RAM (NVRAM) comprising Secure Boot. From Microsoft Learn's Secure boot article (not my writing).
- Key Exchange Key (KEK)
- Database of signing keys that can be used to update the signature database (db) and revoked signatures database (dbx).
- The PK can be used to sign updates to the KEK or to turn off Secure Boot.
- Signature Database (db) and Forbidden Signature Database (dbx)
- Lists approved or rejected signers or image hashes of UEFI applications, operating system loaders, and UEFI drivers that can be loaded on the device.
- The dbx takes precedent if a hash is present in both databases.
The result of this allows OS manipulation that is generally outside the realm of Endpoint Detection and Response (EDR) or eXtended Detection and Response (XDR) tools as they are not yet operational during this phase of the boot process. They don't come into existence until OS drivers are loaded. This also introduces the opportunity for persistent UEFI bootkits that can survive system reformats or hard drive replacements.
Scanning the Enterprise
Though enterprise environments likely have professional EDR/XDR tools updated and looking for this, I was curious what I could find with the basic Binarly PowerShell one-liner they provided for testing. Additionally, I sometimes work in environments that are not enterprise managed and may not have EDR/XDR tools and/or no one is looking at the logs or alerts. With PowerShell remoting I had access and scanned over 2000 systems and fortunately didn't see any offending UEFI implementations. That is of course contingent on the PowerShell hack job I discuss below being correct. From what several manufacturers reported in the linked Ars article, it seems this is an issue primarily on older or often end-of-life/service systems.
Checking for PKfail, you do need local admin. Also remember that if the system you're checking has a BIOS and not UEFI then this isn't applicable. On Windows if you launch an elevated PowerShell prompt you can run the following code snippet from Binarly. If it returns true
, then you're affected. The code prints the ASCII characters of the PK and then uses a basic regular expression to search for the two offending strings.
[System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI PK).bytes) -match "DO NOT TRUST|DO NOT SHIP"
On Linux you can use this one-liner also provided by Binarly. For my Ubuntu Debian based distro I had to install the efitools
package first. This command simply prints the text of the PK signature. If you grep
for the offending string you could mimic the true|false nature of the PowerShell version.
efi-readvar -v PK
Scanning the Enterprise... not so Fast
If you're wanting to scan a domain of Active Directory joined systems it would seemingly be straightforward, but in my environment a PowerShell security feature stopped me in my tracks. It's called constrained language mode (CLM) and is something I've dealt with before related to object serialization, but that's a different issue. From the link:
PowerShell Constrained Language is a language mode of PowerShell designed to support day-to-day administrative tasks, yet restrict access to sensitive language elements that can be used to invoke arbitrary Windows APIs.
Further from the PowerShell Team at Microsoft's Dev Blogs, this exists because PowerShell has “… extended language features that can lead to unverifiable code execution such as direct .NET scripting, invocation of Win32 APIs via the Add-Type cmdlet, and interaction with COM objects.” For security CLM is a good thing, but it can make certain administrative tasks more cumbersome.
As far as I can tell if you run a local interactive shell, CLM is not enabled and you have the full PowerShell capability. If the $ExecutionContext.SessionState.LanguageMode
variable returns FullLanguage
there are no PowerShell limits. However, when remoting non-interactively (i.e., via scripting with Invoke-Command
, for instance) and checking the same variable, you'll get a ConstrainedLanguage
result returned. Interestingly this wasn't consistent if I scanned a single system or PS remoted interactively. I'm not sure why and that was a rabbit hole I didn't explore further.
Constrained Language Mode Workaround Hackjob
Because of constrained language mode I threw together a hacky alternative that includes a local ASCII hash lookup table. It doesn't produce the exact same result as the [System.Text.Encoding]::ASCII.GetString()
method listed above, but it was similar-ish. The problem I think is ASCII is 7-bits but the above method handles 8-bit. I don't know how it translates bits 128 - 255 or the unprintable ASCII characters. I tried using both a '?' and a space. Anyway, I think my mod is good enough as I was able to test it on known, contiguous string characters (e.g., HP Inc.). This would also include (I hope) finding basic strings like “DO NOT TRUST” or “DO NOT SHIP” as that's all it's looking for. For security purposes this is good (not so much for testing) but as of this writing I never found any positive positive cases. Maybe in time I'll find a case and can confirm my code though the Binarly folks suggested it's only present on about 8% of the known firmware from their datasets.
My PowerShell script can be run in an Active Directory (AD) based domain. If the Vulnerable
column returns 'True' then you've found a UEFI that uses the vulnerable PK. If the Vulnerable column is empty that means the system in question has a BIOS so the PK check is ignored. I tested a couple different ways to handle systems that had a BIOS instead, including try/catch blocks, but code wise I think the cleanest was the Get-ComputerInfo
cmdlet and test for it directly. Though I know I have also seen some older Windows Server versions that don't have that specific cmdlet so it's not perfect either. If a system indicates it has FullLanguage
mode then I also prefer to run the Binarly one-liner code snippet first before reverting to the function I have with the ASCII lookup table. Here are some example use cases.
Running the Script
If you're on a single domain AD environment with a single domain you can run the script as-is. PowerShell Remoting must also be enabled, the activedirectory
PowerShell module available, and you need local admin on your target systems.
.\Check-UEFIpk.ps1
This is the same as the first example but with a table format and a cleaner output. Make sure to have the viewable console area wide enough or the output may be truncated. You can add this to any of these commands or alternatively pipe the result for CSV output using Export-CSV
.
.\Check-UEFIpk.ps1 |
Sort-Object CsModel |
Format-Table -AutoSize
If you're using this in a single AD domain environment but want to focus on a specific organization unit (OU) of computer objects, use the OUName
option:
.\Check-UEFIpk.ps1 -OUName <name>
If you review the code, there are some other script options but it's more custom and likely are not applicable for general environments.