PixieFail: Nine Vulnerabilities UEFI Implementations
blog.quarkslab.com
blog.quarkslab.com
(To be clear, I overwhelmingly prefer FOSS and open specs, but we need to be clear about what things solve what problems)
The only major thing it can't boot to my knowledge is Windows, but it might be doable if you chain-load Grub.
What makes x86 feel nicer is that most of the peripherals fall into two categories: 1) Standardized for the arch, so their configuration can be assumed and hardcoded, or 2) Attached to a bus that can be dynamically enumerated (i.e. USB, PCI, etc.) This generally makes the per-system configuration relatively simple.
Embedded systems are not like this. Each SoC is effectively its own system architecture that just happens to share a CPU instruction set with other systems, and the devicetree ends up being the description of this architecture. So it looks gross and ugly and complex by comparison, but the complexity of defining peripherals for a system has to exist somewhere!
The messy bits are, of course, that both the OS and bootloader need access to subsets of this data, and you'd like to ship the tables with the bootloader for a SoC (so the OS can be gneeric and not care), but really, the bootloader only needs a little bit of this, and the OS needs almost all of it... and this leads to coupling between the two being tighter than it should be.
Care to elaborate? I only have a passing interest in the ACPI / UEFI / Open Firmware / device trees / etc. story, but I haven’t encountered anything particularly awful about device trees, or (until you) anybody saying there was.
While there are alternatives, you generally seem to get "device trees and a barebones bootloader" on ARM and "UEFI + ACPI" on amd64.
ACPI will list hardware and necessary hardware properties based on some basic API calls to the system interface. UEFI initialises the ACPI data structure and exposes it to the bootloader so the appropriate drivers can be loaded and configured.
With device trees, you basically configure and build the drivers and configuration into the kernel/OS you're trying to load. That's why compiling Linux on amd64 is generally easy and produces a single image, while for many other devices (smartphones, some SBCs) you need to compile a kernel per device. The device trees only need to be imported/written once per device (or device type, depending on how nice the manufacturers are), but that's how you get stuff like this: https://github.com/torvalds/linux/tree/master/arch/arm64/boo...
On ARM there are actually a few devices that implement UEFI, but most of them have Secure Boot locked in and configured to only boot Windows.
ACPI is not perfect and it's not technically required to have UEFI to implement something better than device trees, but I'm not sure if reinventing the wheel here is necessary or even preferable. UEFI already has open source implementations ready to go, with kernels and other tools already containing code to interact with those APIs, whereas a custom ACPI replacement protocol would need more implementation work,
"Much of the firmware ACPI functionality is provided in bytecode of ACPI Machine Language (AML), a Turing-complete, domain-specific low-level language, stored in the ACPI tables.[7] To make use of the ACPI tables, the operating system must have an interpreter for the AML bytecode."[1]
And it's not like this was intended to truly be fully interoperable. "Maybe we could define the APIs so that they work well with NT and not the others even if they are open."[2]
ARM has none of that: no standardized platform, no device discovery through bus enumeration, no standard VESA graphics implementation. Device tree is a consequence of the non-standardization of ARM devices, not the cause. You could easily have the devicetree in the phone's ROM (just like ACPI, but without the executable code vector) and have the kernel read that data on startup. But you would need a kernel of at least the same size as the PC kernel+initramfs; and unlike on PC's, on phones people still (somewhat) care about code size and performance.
https://news.ycombinator.com/item?id=26114417
https://support.apple.com/guide/security/boot-process-secac7...
If you hate edk2, you can run something else e.g. u-boot, the linux kernel or your own code.
If you hate opensbi, look at oreboot, which implements something equivalent to u-boot SPL + opensbi in rust.
Needing a separate build for nearly every different SBC sucks and I'm happy about anything that will broaden compatibility here.
So, Linux's implementation would work well, whilst Windows has to pretend to be Linux in order to have it work mostly kinda unless the device explicitly supported Windows?
[0] Weirdly not all upstreamed last I looked, but mostly.
[1] I mean, you might be able to put a Pi-compatible bootloader and, say, a UEFI+ARM bootloader on the same stick, but I mean with a single boot path.
Wikipedia explains:
> Intel Active Management Technology, part of Intel vPro, implements out-of-band management, giving administrators remote administration, remote management, and remote control of PCs with no involvement of the host processor or BIOS, even when the system is powered off. Remote administration includes remote power-up and power-down, remote reset, redirected boot, console redirection, pre-boot access to BIOS settings, programmable filtering for inbound and outbound network traffic, agent presence checking, out-of-band policy-based alerting, access to system information, such as hardware asset information, persistent event logs, and other information that is stored in dedicated memory (not on the hard drive) where it is accessible even if the OS is down or the PC is powered off. Some of these functions require the deepest level of rootkit, a second non-removable spy computer built around the main computer. Sandy Bridge and future chipsets have "the ability to remotely kill and restore a lost or stolen PC via 3G". Hardware rootkits built into the chipset can help recover stolen computers, remove data, or render them useless, but they also present privacy and security concerns of undetectable spying and redirection by management or hackers who might gain control.
Well, except for the 100s of bugs in vPro that cause it to completely insecure...
> 2023-11-14 Quarkslab replied to the prior requests and commentary from various vendors as follows: Stated that the blog post about the issues would contain the technical report submitted to the disclosure coordination forum and a detailed timeline of the relevant events in the disclosure process. It would include proof-of-concept code to trigger vulnerabilities 1 to 7 but NOT exploit code. Reiterated that the purpose of reporting the vulnerabilities was to help vendors identify and fix them, not to debate about the editorial policies for Quarkslab research work.
I have no interest whatsoever to install MS Windows, is there any motherboards out there for AMD64/X86_64 that comes without UEFI/BIOS?
Your computer needs UEFI/BIOS for hardware initialization and launching your boot process.
There is a open implementation tho, see https://libreboot.org/
> The Libreboot project provides free, open source (libre) boot firmware based on coreboot, replacing proprietary BIOS/UEFI firmware on specific Intel/AMD x86 and ARM based motherboards, including laptop and desktop computers. It initialises the hardware (e.g. memory controller, CPU, peripherals) and starts a bootloader for your operating system.
You're going to need firmware anyway, otherwise your OS implements the firmware responsibilities, and you then GOTO 1, and have other problems.
Most UEFI vulnerabilities seem to come from bugs in the open source reference implementation making it down to the proprietary firmware.
That said: as much as you would expect from any low-level C program. I hope that EDK will eventually find itself rewritten in a safer language to it easier to spot mistakes. I would say Rust would be the best fit, but I admit that it's not a perfect fit; sadly, we lack safe low-level languages.
As for running without UEFI/BIOS: most ARM devices have an alternate bootloader (usually uBoot with some proprietary magic) but I don't know if that's much better. This approach usually requires per-device support from the operating system you want to install.
Rust is a safe low-level language. You can start writing your UEFI implementation in rust today [1], [2]
> As for running without UEFI/BIOS: most ARM devices have an alternate bootloader
UEFI/BIOS is not a bootloader, they initialize hardware and then pass on responsibility to a bootloader. Popular bootloaders are the Windows bootloader, grub, etc etc.
I see Rust as a C++ replacement more than a C replacement. I'm not sure if object oriented languages (well, "struct with functions" in Rust's case) with vtables and dynamic allocation and the like are a good match for that kind of code. Rust, as a language, simply can't deal with running out of memory; all allocations are assumed to succeed, which is a dangerous assumption, especially in low-level code like this.
Unfortunately, we don't really have a common C replacement with Rust-like safety guarantees. I'm hoping Zig will be able to provide some safety to the C ecosystem, but it's still a ways away from 1.0.
> UEFI/BIOS is not a bootloader
You're right, I meant "bootstrapping system". I'm sure there's better terminology here but the word escapes me at the moment.
One could, of course, write their own allocator and heap tracking system as part of the UEFI firmware, or reimplement the entire thing without any of the standard library and simply relying on the core library, but if you want to deal with fallibility, you'll have to reimplement every part of the language toolkit and use Option<> or Result<> for every API call, which would only complicate the rewrite more.
> you'll have to reimplement every part of the language toolkit
You shouldn't need to, those APIs already exist. Like https://doc.rust-lang.org/stable/std/boxed/struct.Box.html#m... for example. Maybe there's some coverage missing, I personally use no heap when I'm working in this context.
The chromebook variant already runs coreboot, with all changes upstreamed: https://frame.work/blog/introducing-the-framework-laptop-chr...
That would be cool for many different reason! And pretty fugly for some others...
I'm working on that for one of my projects. A BIOS can use GPT partitions to have LBA48, but MBR partitions standards could laso be changed to use LBA48 instead of LBA32 which cause the limit
You could even use the CHS fields to have even more space if needed: there are 3 bytes for the CHS start, 3 others for the CHS end, both are often wasted by storing 0 for LBA32.
That said, I still consider UEFI to be better for at least 1) multiplatform support (have different /EFI/BOOT paths to payloads named by their architecture) 2) fallbacks (shell + startup.nsh) 3) defining how the persistence of settings work (efivars)
* Not having "extended partitions" as a concept to deal with
* OS booting not dependant on "load an MSDOS bootblock from LBA offset X"
What I dislike:
* Not having a minimal straightforward shell to troubleshoot from
* Not letting the UEFI variables be easily user accessible/fixable
You can BIOS boot from a GPT-partitioned disk just fine.
> OS booting not dependant on "load an MSDOS bootblock from LBA offset X"
The only offset that's hardcoded by the BIOS is LBA block 0. The rest is from your bootloader. And GPT partitioning allows you to put the bootloader code in a proper partition rather than in the void under the stairs: https://en.wikipedia.org/wiki/BIOS_boot_partition
> Not having a minimal straightforward shell to troubleshoot from
Download shellx64.efi from the (Intel) EFI development kit. When it's present in the root directory of your EFI System Partition, most firmwares will include an option to boot into that shell. Or is your objection that it's an DOS-inspired command prompt rather than a Unix shell?
> Not letting the UEFI variables be easily user accessible/fixable
Don't know about this one. The linux kernel supports reading and writing EFI variables through efivarfs, but I don't know if that is predicated on firmware support and whether it includes access to all variables.
There is a shell for UEFI. It feels a lot like a reimplementation of MS-DOS shell, with "FS0:" to change drives etc, but it has directory listings, file copying, etc.
The interface is standardized and there's an open source implementation in EDK2.
If you build Linux kernels as "EFISTUB", they're simply executables in the UEFI shell.
https://github.com/tianocore/tianocore.github.io/wiki/ShellP...
There are plenty of other firmware systems that allow for the same thing, and I dislike UEFI as much as the next guy, but this is a feature that's been important at work.
There were tons of other ways this could have been accomplished for far less effort and with previously existing systems.
I don't say UEFI is no over complicated, over engineered mess but I worked on embedded systems for a while and I'd say it's no sunshine there either. Booting a modern OS on modern hardware involves a lot of essential complexity you cannot get away from.
Systems that are inherently complex tend to naturally accrue accidental complexity. It's understandably hard to keep these systems lean.
My point is that modern boot systems in general fall into the second category, contrary to other comments that imply the first.
It's not a single microcontroller you're powering on, even if it were, those too require a bunch of initialisation code to function properly.
Previously, we had motherboards do the same stuff, except now they called into ROM memory on the network card, and ran a bunch of non-standard, proprietary code to render the fancy graphics and do online updating. I don't believe for a second that things were easier before UEFI standardised it all, they were just hidden from plain view better.
TL;DR a BIOS doesn't have to boot a drive if there's no primary partition marked "active", no matter whether your boot code uses that or not.
Just directboot a static kernel and kexec() the real kernel -- like petitboot has been doing since forever: https://github.com/open-power/petitboot/blob/master/README.m...
Unless you like writing everything (display drivers, disk drivers, filesystem drivers, network stacks...) twice. Which is where these bugs come from...
TL;DR you can make it simple if you close the platform (this is not the same as open source Vs closed source) - open platforms end up developing complex interfaces so thatthey are actually open to end owners.
Likewise, demand and use cases for network boot exist, otherwise it wouldn't be here. Same goes for every other feature most users would consider bloat.
You can network boot too; just run `busybox udhcpc`.
I think you misread my comment. I never described signature-checking or network boot as bloat. I said it was stupid to have to implement these things twice (once in mainline Linux and then all over again in kooky UEFI-land with its bizzarre API, ABI, and wacky rules).
I still think it is stupid to do that, because it is. We have working, high-quality, battle-tested implementations of all this stuff. Use them.
Edit: On reading another reply, I see your argument for having fewer implementations; that's not nothing, but it only really helps if everything is on Linux, which isn't going to help most desktops (and I hope we can agree that NT in firmware is not better) or the rest of the world that isn't on Linux (say, the BSDs).
A partial effort has already been made a while back: https://github.com/tianocore/edk2-staging/tree/edkii-rust
However, this uses uefi-rs, which is incompatible with TianoCore's BSD+Patent licensing, and therefore cannot be used as reference material, as the wiki page states: https://github.com/tianocore/tianocore.github.io/wiki/Tasks-...
More recent efforts have also been mentioned in the mailing list: https://edk2.groups.io/g/devel/search?p=recentpostdate%2Fsti... Rust also has a basic standard library implementation now: https://github.com/rust-lang/rust/pull/105861
I'm not sure about the weak pseudo-RNG, because I would expect an existing crate to get imported for that use case, but the same RNG could also be implemented just as badly in Rust.
As for the buffer overflow vulnerabilities: I completely agree. These are the most dangerous vulnerabilities and I doubt they would've made it past the compiler had they been written in Rust.